BabylonJS/Spector.js

View on GitHub
extensions/contentScript.js

Summary

Maintainability
D
2 days
Test Coverage
var debug = false;

//_______________________________EXTENSION POLYFILL_____________________________________
window.browser = (function () {
    return window.msBrowser ||
        window.browser ||
        window.chrome ||
        browser;
})();

function getExtensionURL() {
    return window.browser.extension.getURL("");
};

var uniqueId = new Date().getTime() + Math.abs(Math.random() * 1000000);
function sendMessage(message, cb) {
    message["uniqueId"] = uniqueId;
    window.browser.runtime.sendMessage(message, function (response) {
        if (cb) {
            cb(response);
        }
    });
};

function listenForMessage(callback) {
    window.browser.runtime.onMessage.addListener(callback);
};
//_____________________________________________________________________________________

//_______________________________SCRIPT  UTILITIES_____________________________________
function insertTextScript(text) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.text = text;
    insertHeaderNode(script);
    return script;
};

function insertScript(url) {
    var script = document.createElement("script");
    script.type = "text/javascript";

    // Place the entire inspector into a <script> tag instead of referencing
    // the URL - this enables synchronous loading and inspection of code
    // creating contexts before dom ready.
    // It's nasty, though, as dev tools shows it as another script group on
    // the main page.
    // script.src = url;
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, false);
    xhr.send('');
    script.text = xhr.responseText;

    insertHeaderNode(script);
    return script;
};

function insertHeaderNode(node) {
    var targets = [document.body, document.head, document.documentElement];
    for (var n = 0; n < targets.length; n++) {
        var target = targets[n];
        if (target) {
            if (target.firstElementChild) {
                target.insertBefore(node, target.firstElementChild);
            } else {
                target.appendChild(node);
            }
            break;
        }
    }
};
//_____________________________________________________________________________________

var spectorLoadedKey = "SPECTOR_LOADED";
var spectorCaptureOnLoadKey = "SPECTOR_CAPTUREONLOAD";
var spectorCaptureOnLoadCommandCountKey = "SPECTOR_CAPTUREONLOAD_COMMANDCOUNT";
var spectorCaptureOnLoadTransientKey = "SPECTOR_CAPTUREONLOAD_TRANSIENT";
var spectorCaptureOnLoadQuickCaptureKey = "SPECTOR_CAPTUREONLOAD_QUICKCAPTURE";
var spectorCaptureOnLoadFullCaptureKey = "SPECTOR_CAPTUREONLOAD_FULLCAPTURE";
var captureOffScreenKey = "SPECTOR_CAPTUREOFFSCREEN";
var spectorCommunicationElementId = "SPECTOR_COMMUNICATION";
var spectorCommunicationQuickCaptureElementId = "SPECTOR_COMMUNICATION_QUICKCAPTURE";
var spectorCommunicationFullCaptureElementId = "SPECTOR_COMMUNICATION_FULLCAPTURE";
var spectorCommunicationCommandCountElementId = "SPECTOR_COMMUNICATION_COMMANDCOUNT";
var spectorCommunicationRebuildProgramElementId = "SPECTOR_COMMUNICATION_REBUILDPROGRAM";

var spectorContextTypeKey = "__spector_context_type";

var captureOnLoad = false;
var captureOnLoadTransient = false;
var captureOnLoadQuickCapture = false;
var captureOnLoadFullCapture = false;
var captureOnLoadCommandCount = 500;
var captureOffScreen = false;

if (sessionStorage.getItem(spectorCaptureOnLoadKey) === "true") {
    sessionStorage.setItem(spectorCaptureOnLoadKey, "false");
    captureOnLoad = true;

    captureOnLoadTransient = (sessionStorage.getItem(spectorCaptureOnLoadTransientKey) === "true");
    captureOnLoadQuickCapture = (sessionStorage.getItem(spectorCaptureOnLoadQuickCaptureKey) === "true");
    captureOnLoadFullCapture = (sessionStorage.getItem(spectorCaptureOnLoadFullCaptureKey) === "true");
    captureOnLoadCommandCount = parseInt(sessionStorage.getItem(spectorCaptureOnLoadCommandCountKey));
}

captureOffScreen = (sessionStorage.getItem(captureOffScreenKey) === "true");

var canvasGetContextDetection = `
    var spector;
    var captureOnLoad = ${captureOnLoad ? "true" : "false"};
    var captureOffScreen = ${captureOffScreen ? "true" : "false"};
    window.__SPECTOR_Canvases = [];

    (function() {
        var __SPECTOR_Origin_EXTENSION_GetContext = HTMLCanvasElement.prototype.getContext;
        HTMLCanvasElement.prototype.__SPECTOR_Origin_EXTENSION_GetContext = __SPECTOR_Origin_EXTENSION_GetContext;

        if (typeof OffscreenCanvas !== 'undefined') {
            var __SPECTOR_Origin_EXTENSION_OffscreenGetContext = OffscreenCanvas.prototype.getContext;
            OffscreenCanvas.prototype.__SPECTOR_Origin_EXTENSION_OffscreenGetContext = __SPECTOR_Origin_EXTENSION_OffscreenGetContext;

            OffscreenCanvas.prototype.getContext = function () {
                var context = null;
                if (!arguments.length) {
                    return context;
                }
    
                if (arguments.length === 1) {
                    context = this.__SPECTOR_Origin_EXTENSION_OffscreenGetContext(arguments[0]);
                    if (context === null) {
                        return context;
                    }
                }
                else if (arguments.length === 2) {
                    context = this.__SPECTOR_Origin_EXTENSION_OffscreenGetContext(arguments[0], arguments[1]);
                    if (context === null) {
                        return context;
                    }
                }
    
                var contextNames = ["webgl", "experimental-webgl", "webgl2", "experimental-webgl2"];
                if (contextNames.indexOf(arguments[0]) !== -1) {
                    // context.canvas.setAttribute("${spectorContextTypeKey}", arguments[0]);
                    // Notify the page a canvas is available.
                    var myEvent = new CustomEvent("SpectorWebGLCanvasAvailableEvent");
                    document.dispatchEvent(myEvent);
                    this.id = "Offscreen";
                    window.__SPECTOR_Canvases.push(this);
    
                    if (captureOnLoad) {
                        // Ensures canvas is in the dom to capture the one we are currently tracking.
                        if (${captureOnLoadTransient}) {
                            spector.captureContext(context, ${captureOnLoadCommandCount}, ${captureOnLoadQuickCapture}, ${captureOnLoadFullCapture});
                            captureOnLoad = false;
                        }
                    }
                }
    
                return context;
            }
        }

        HTMLCanvasElement.prototype.getContext = function () {
            var context = null;
            if (!arguments.length) {
                return context;
            }

            if (arguments.length === 1) {
                context = this.__SPECTOR_Origin_EXTENSION_GetContext(arguments[0]);
                if (context === null) {
                    return context;
                }
            }
            else if (arguments.length === 2) {
                context = this.__SPECTOR_Origin_EXTENSION_GetContext(arguments[0], arguments[1]);
                if (context === null) {
                    return context;
                }
            }

            var contextNames = ["webgl", "experimental-webgl", "webgl2", "experimental-webgl2"];
            if (contextNames.indexOf(arguments[0]) !== -1) {
                context.canvas.setAttribute("${spectorContextTypeKey}", arguments[0]);
                // Notify the page a canvas is available.
                var myEvent = new CustomEvent("SpectorWebGLCanvasAvailableEvent");
                document.dispatchEvent(myEvent);

                if (captureOffScreen) {
                    var found = false;
                    for (var i = 0; i < window.__SPECTOR_Canvases.length; i++) {
                        if (window.__SPECTOR_Canvases[i] === this) {
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        window.__SPECTOR_Canvases.push(this);
                    }
                }

                if (captureOnLoad) {
                    // Ensures canvas is in the dom to capture the one we are currently tracking.
                    if (this.parentElement || ${captureOnLoadTransient}) {
                        spector.captureContext(context, ${captureOnLoadCommandCount}, ${captureOnLoadQuickCapture}, ${captureOnLoadFullCapture});
                        captureOnLoad = false;
                    }
                }
            }

            return context;
        }
    })()`;
insertTextScript(canvasGetContextDetection);

var frameId = null;

// In case the spector injection has been requested, inject the library in the page.
if (sessionStorage.getItem(spectorLoadedKey)) {
    
    if (debug) {
        insertScript("http://localhost:1337/tools/loader.js");
    }
    else {
        insertTextScript( '(' + spectorBundleHook.toString() + ' )();');
    }

    // Defer exec to next slot to ensure proper loading of the lib.
    setTimeout(function () {
        var captureLib = `spector = new SPECTOR.Spector();
            spector.spyCanvases();
            document.addEventListener("SpectorRequestPauseEvent", function() {
                spector.pause();
            });
            document.addEventListener("SpectorRequestPlayEvent", function() {
                spector.play(); 
            });
            document.addEventListener("SpectorRequestPlayNextFrameEvent", function() {
                spector.playNextFrame(); 
            });    
            document.addEventListener("SpectorRequestCaptureEvent", function(e) {
                var canvasIndex = document.getElementById('${spectorCommunicationElementId}').value;

                var canvas = null;
                if (${captureOffScreen}) {
                    canvas = window.__SPECTOR_Canvases[canvasIndex];
                } else {
                    canvas = document.body.querySelectorAll("canvas")[canvasIndex]; 
                }
                var quickCapture = (document.getElementById('${spectorCommunicationQuickCaptureElementId}').value === "true");
                var fullCapture = (document.getElementById('${spectorCommunicationFullCaptureElementId}').value === "true");
                var commandCount = 0 + document.getElementById('${spectorCommunicationCommandCountElementId}').value;

                spector.captureCanvas(canvas, commandCount, quickCapture, fullCapture);
            });
            document.addEventListener("SpectorRequestRebuildProgramEvent", function(e) {
                var buildInfoInText = document.getElementById('${spectorCommunicationRebuildProgramElementId}').value;
                var buildInfo = JSON.parse(buildInfoInText);
                var tabId = document.getElementById('${spectorCommunicationElementId}').value;

                var programId = buildInfo.programId;
                var sourceVertex = buildInfo.sourceVertex;
                var sourceFragment = buildInfo.sourceFragment;
                spector.rebuildProgramFromProgramId(programId,
                    sourceVertex,
                    sourceFragment,
                    (program) => {
                        spector.referenceNewProgram(programId, program);
                        var myEvent = new CustomEvent("SpectorOnProgramRebuilt", { detail: { programId: programId, errorString: null, tabId: tabId } });
                        document.dispatchEvent(myEvent);
                    },
                    (error) => {
                        var myEvent = new CustomEvent("SpectorOnProgramRebuilt", { detail: { programId: programId, errorString: error, tabId: tabId } });
                        document.dispatchEvent(myEvent);
                    });
            });
            document.addEventListener("SpectorRequestCanvasListEvent", function(e) {
                var canvasList = [];
                for (var i = 0; i < window.__SPECTOR_Canvases.length; i++) {
                    var canvas = window.__SPECTOR_Canvases[i];
                    canvasList.push({
                        id: canvas.id,
                        width: canvas.width,
                        height: canvas.height,
                        ref: i
                    });
                }
                var myEvent = new CustomEvent("SpectorOnCanvasListEvent", { 
                    detail: {
                        canvasList: canvasList 
                    }
                });
                document.dispatchEvent(myEvent);
            });
            spector.onError.add((error) => {
                var myEvent = new CustomEvent("SpectorOnErrorEvent", { detail: { errorString: error } });
                document.dispatchEvent(myEvent);
            });
            spector.onCapture.add((capture) => {
                var myEvent = new CustomEvent("SpectorOnCaptureEvent", { detail: { capture: capture } });
                document.dispatchEvent(myEvent);
            });
            setInterval(() => {
                var myEvent = new CustomEvent("SpectorFPSEvent", { detail: { fps: (spector ? spector.getFps() : 0) } });
                document.dispatchEvent(myEvent);
            }, 1500);
            window.spector = spector;`;

        if (debug) {
            insertTextScript(`SPECTORTOOLS.Loader
            .onReady(function() {
                ${captureLib}
            })
            .load();`);
        }
        else {
            insertTextScript(captureLib);
        }
    }, 0);

    document.addEventListener("DOMContentLoaded", function () {
        var script = `var input = document.createElement('input');
        input.type = 'Hidden';
        input.id = '${spectorCommunicationElementId}';
        document.body.appendChild(input);
        var input2 = document.createElement('input');
        input2.type = 'Hidden';
        input2.id = '${spectorCommunicationQuickCaptureElementId}';
        document.body.appendChild(input2);
        var input3 = document.createElement('input');
        input3.type = 'Hidden';
        input3.id = '${spectorCommunicationFullCaptureElementId}';
        document.body.appendChild(input3);
        var input4 = document.createElement('input');
        input4.type = 'Hidden';
        input4.id = '${spectorCommunicationRebuildProgramElementId}';
        document.body.appendChild(input4);
        var input5 = document.createElement('input');
        input5.type = 'Hidden';
        input5.id = '${spectorCommunicationCommandCountElementId}';
        document.body.appendChild(input5);`;

        insertTextScript(script);
    });

    function sendChunks(serialized, buildMsg) {
        var len = serialized.length;
        var ii = 0;
        var step = 32 * 1024 * 1024; // 32 MB
        while (ii < len) {
            var nextIndex = Math.min(ii + step, len);
            var substr = serialized.substring(ii, nextIndex);
            const msg = buildMsg(substr);
            sendMessage(msg);
            ii = nextIndex;
        }
    }

    function buildCaptureChunkMessage(substr) {
        return { captureChunk: substr };
    }

    /**
     * For very large captures, the serialized size can exceed the maximum string length for the browser.  This
     * attempts to get around that by splitting up the inner `commands` array (which might be very large) and
     * sending it separately in chunks.
     */
    function serializeCapture(capture) {
        var commands = capture.commands;
        capture.commands = [];

        var serializedCapture;
        try {
            serializedCapture = JSON.stringify(capture);
        } catch(err) {
            throw new Error('Capture serialization unable to stringify; capture failed: ' + err);
        }

        sendChunks(serializedCapture, buildCaptureChunkMessage);

        // Send chunks of 100 commands at a time.  They will be re-joined to the capture on the other side.
        var commandChunkSize = 100;
        for (var i = 0; i < commands.length; i += commandChunkSize) {
            var chunk = [];
            for (var j = 0; j < commandChunkSize; j++) {
                if (i + j < commands.length) {
                    chunk.push(commands[i + j]);
                    // Remove the command from the original array to allow it to be garbage collected early.
                    commands[i + j] = null;
                } else {
                    break;
                }
            }

            var chunkSerialized;
            try {
                chunkSerialized = JSON.stringify(chunk);
            } catch(err) {
                throw new Error('Capture serialization unable to stringify; capture failed: ' + err);
            }

            function buildCommandChunkMessage(substr) {
                return { commandChunk: substr, chunkIx: i / commandChunkSize };
            }
            sendChunks(chunkSerialized, buildCommandChunkMessage);
        }
    };

    document.addEventListener('SpectorOnCaptureEvent', function (e) {
        // The browser imposes limits on the size of the serialized JSON
        // associated with these messages. To avoid running into these limits,
        // and not have to introspect too deeply into the ICapture structure, we
        // manually serialize to JSON and chop up the resulting string.
        //
        // Note for future reference: the object that comes in via
        // e.detail.capture is actually immutable, because of the way it's
        // serialized from the main world to the content script. If the goal
        // were to stub out certain fields and send them separately,
        // Object.assign would have to be used to create a new top-level object.

        serializeCapture(e.detail.capture);
        sendMessage({ captureDone: true });
    }, false);

    document.addEventListener('SpectorOnErrorEvent', function (e) {
        sendMessage({ errorString: e.detail.errorString });
    }, false);

    document.addEventListener('SpectorFPSEvent', function (e) {
        sendMessage({ fps: e.detail.fps });
    }, false);

    document.addEventListener('SpectorOnProgramRebuilt', function (e) {
        sendMessage({ 
            programRebuilt: {
                programId: e.detail.programId, 
                errorString: e.detail.errorString 
            },
            tabId: e.detail.tabId
        });
    }, false);

    document.addEventListener('SpectorOnCanvasListEvent', function(e) {
        var canvasList = e.detail.canvasList;

        var uiInformation = [];
        for (var i = 0; i < canvasList.length; i++) {
            var canvasInformation = canvasList[i];
            uiInformation.push({
                id: canvasInformation.id,
                width: canvasInformation.width,
                height: canvasInformation.height,
                ref: canvasInformation.ref
            });
        }

        // Inform the extension that canvases are present (2 means injection has been done, 1 means ready to inject)
        sendMessage({ canvases: uiInformation, captureOffScreen: true }, function (response) {
            frameId = response.frameId;
        });
    });
}
else {
    document.addEventListener('SpectorWebGLCanvasAvailableEvent', function(e) {
        // Inform the extension that canvases are present (2 means injection has been done, 1 means ready to inject)
        sendMessage({ present: 1 }, function (response) {
            frameId = response.frameId;
        });
    }, false);
}

var refreshCanvases = function() {
    if (captureOffScreen) {
        // List is retrieved from all the ever created canvases.
        var myEvent = new CustomEvent("SpectorRequestCanvasListEvent");
        document.dispatchEvent(myEvent);
    }
    else {
        if (document.body) {
            var canvasElements = document.body.querySelectorAll("canvas");
            if (canvasElements.length > 0) {

                var canvasesInformation = [];
                for (var i = 0; i < canvasElements.length; i++) {
                    var canvas = canvasElements[i];
                    var context = null;
                    try {
                        context = canvas.getContext(canvas.getAttribute(spectorContextTypeKey));
                    }
                    catch (e) {
                        // Do Nothing.
                    }
                    
                    if (context) {
                        canvasesInformation.push({
                            id: canvas.id,
                            width: canvas.width,
                            height: canvas.height,
                            ref: i
                        });
                    }
                }
                sendMessage({ canvases: canvasesInformation, captureOffScreen: false }, function (response) {
                    frameId = response.frameId;
                });
            }
        }
    }
}

// Check for existing canvas a bit after the end of the loading.
document.addEventListener("DOMContentLoaded", function () {
    if (sessionStorage.getItem(spectorLoadedKey)) {
        // Inform the extension that canvases are present (2 means injection has been done, 1 means ready to inject)
        sendMessage({ present: 2 }, function (response) {
            frameId = response.frameId;
        });

        // Refresh the canvas list.
        setTimeout(function () {
            sendMessage({ pageReload: true }, function (response) {
                frameId = response.frameId;
            });
        }, 500);
    }
});

listenForMessage(function (message) {
    var action = message.action;
    // Only answer to actions.
    if (!action) {
        return;
    }

    // We need to reload to inject the scripts.
    if (action === "pageAction") {
        if (!sessionStorage.getItem(spectorLoadedKey)) {
            sessionStorage.setItem(spectorLoadedKey, "true");
            // Delay for all frames.
            setTimeout(function () { window.location.reload(); }, 50);
            return;
        }
    }

    // Set offscreen canvas mode.
    if (action === "changeOffScreen") {
        sessionStorage.setItem(captureOffScreenKey, message.captureOffScreen ? "true" : "false");
        // Delay for all frames.
        setTimeout(function () { window.location.reload(); }, 50);
        return;
    }

    // We need to reload to inject the capture loading sequence.
    if (action === "captureOnLoad") {
        var transient = message.transient;
        var commandCount = message.commandCount;
        var quickCapture = message.quickCapture;
        var fullCapture = message.fullCapture;

        sessionStorage.setItem(spectorCaptureOnLoadTransientKey, transient);
        sessionStorage.setItem(spectorCaptureOnLoadQuickCaptureKey, quickCapture);
        sessionStorage.setItem(spectorCaptureOnLoadFullCaptureKey, fullCapture);
        sessionStorage.setItem(spectorCaptureOnLoadCommandCountKey, commandCount);
        sessionStorage.setItem(spectorCaptureOnLoadKey, "true");

        // Delay for all frames.
        setTimeout(function () { window.location.reload(); }, 50);
        return;
    }

    // Let the paused canvas play again. 
    if (action === "playAll") {
        var myEvent = new CustomEvent("SpectorRequestPlayEvent");
        document.dispatchEvent(myEvent);
        return;
    }

    // Let s refresh the canvases list. 
    if (action === "requestCanvases") {
        setTimeout(function () { refreshCanvases(); }, 0);
        setTimeout(function () { refreshCanvases(); }, 1000);
        return;
    }

    // Following actions are only valid for the selected frame.
    var canvasRef = message.canvasRef;
    if (canvasRef.frameId !== frameId) {
        return;
    }

    if (action === "pause") {
        var myEvent = new CustomEvent("SpectorRequestPauseEvent");
        document.dispatchEvent(myEvent);
    }
    else if (action === "play") {
        var myEvent = new CustomEvent("SpectorRequestPlayEvent");
        document.dispatchEvent(myEvent);
    }
    else if (action === "playNextFrame") {
        var myEvent = new CustomEvent("SpectorRequestPlayNextFrameEvent");
        document.dispatchEvent(myEvent);
    }
    else if (action === "capture") {
        var input = document.getElementById(spectorCommunicationElementId);
        if (input) {
            input.value = canvasRef.index;

            var inputQuickCapture = document.getElementById(spectorCommunicationQuickCaptureElementId);
            if (inputQuickCapture) {
                inputQuickCapture.value = message.quickCapture ? "true" : "false";
            }
            var inputFullCapture = document.getElementById(spectorCommunicationFullCaptureElementId);
            if (inputFullCapture) {
                inputFullCapture.value = message.fullCapture ? "true" : "false";
            }
            var inputCommandCount = document.getElementById(spectorCommunicationCommandCountElementId);
            if (inputCommandCount) {
                inputCommandCount.value = message.commandCount;
            }

            var myEvent = new CustomEvent("SpectorRequestCaptureEvent");
            document.dispatchEvent(myEvent);
        }
    }
    else if (action === "rebuildProgram") {
        var input = document.getElementById(spectorCommunicationRebuildProgramElementId);
        var tabIdInput = document.getElementById(spectorCommunicationElementId);
        if (input && tabIdInput) {
            var buildInfo = message.buildInfo;
            var buildInfoInText = JSON.stringify(buildInfo);
            input.value = buildInfoInText;
            tabIdInput.value = canvasRef.tabId;

            var myEvent = new CustomEvent("SpectorRequestRebuildProgramEvent");
            document.dispatchEvent(myEvent);
        }
    }
});