How to determine number of active WebGL contexts? - javascript

Browsers impose limits on the number of active WebGL contexts. Exceed the limit and the browser will start to dump older contexts. My understanding is that there are limits per domain as well as an overall max.
Two questions:
Is there a way to determine how close you are to the limits, i.e., how many active WebGL contexts there are and how many available?
I've found scraps of info here and there, but haven't been able to nail down exactly what the limits are for each browser, both per-domain and max. What are the limits for Chrome, Firefox, Safari, Edge and IE 11?

There is no reliable way to know how many contexts a browser supports and even if you did know it could change tomorrow or it could change based on various conditions of the machine the browser is running on or the browser's own heuristics, like maybe if vram is low it allows less contexts. Or maybe if the newest context uses too many resources it tries to free up space by losing other contexts.
My personal rule of thumb is browsers support at least 8 contexts. That's what I've built my sites to assume.
You can probably understand why there's a limit. WebGL apps use tons of resources. Maybe not all of them but games in particular can easily use gigs of vram and that vram is not easily virutalized like regular ram is, especially since in order to display the results in the browser itself somehow the results all have to make it to the same process. So, since they are likely not virtualized and since they can use so many resources the browser needs to limit how many can be created at once to free up resources for the latest page the user visits.
There are plenty of techinques to use a single context to get display lots of things all over a webpage which are covered or referenced in the Q&A you linked to.
You can count though like this:
const canvases = [];
let done;
function createCanvas() {
const canvas = document.createElement('canvas');
canvas.addEventListener('webglcontextlost', () => {
done = true;
console.log('num contexts:', canvases.length - 1);
});
const gl = canvas.getContext('webgl');
canvases.push(canvas);
}
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
while (!done) {
createCanvas();
await wait();
}
}
main();
On my Macbook Pro Chrome 80, Firefox 75, Safari 13.1, and Safari on an iPhone 11, all reported 16 contexts. Chrome 81 on Android on a Pixel 2 XL reported 8, Firefox on the same device reported 9. But like it says above, all those numbers could change tomorrow or even today under different conditions.
Follow Up
Whatever the browser's limit is seems to be per domain in Chrome but per page in Firefox and Safari. You can test this here. Open 2 windows. Create 16 contexts in one window, then create one more in another. In Chrome you'll see the first window lose a context as soon as the one more context is created in the 2nd window. In Firefox and Safari each window has it's own limit.
I also found lots of variation in other behavior. On Chrome I expected that if I created 16 contexts in one window, 1 in another I'd see one lost in the first window but that when I closed the 2nd window I'd see that one lost context restored in the first window. I don't. I don't know what if anything would trigger that context to get restored.
In Firefox with the code linked above, as soon as I create the 17th context in the same window it goes into an infinite loop. It loses the first context, that context registers to be restored, firefox restores it immediately, which loses another context, repeat. This seems like it makes it impossible to use in Firefox.
Trying another example where I don't keep a reference to the canvases, which means they can be garbage collected, I see in Firefox I never get a context lost event which makes some sense since I no longer have an effective reference to the context there's no reason to send a lost context event. Chrome on the other hand does still send the event which is also technically not wrong since I registered for the event so the event itself still has a reference and if I didn't want to know I should have unregistered the event.
Apparently this is an under specified and under tested part of the WebGL spec.
It looks like the only thing you can really do for a lost context is notify the user they got one and provide them a button to start over (create a new one or refresh the page)
const groups = [];
let done;
function createCanvas(parent, lostCallback) {
const canvas = document.createElement('canvas');
parent.appendChild(canvas);
canvas.addEventListener('webglcontextlost', lostCallback);
const gl = canvas.getContext('webgl');
return {canvas, gl};
}
function createGroup() {
const div = document.createElement('div');
const group = {div};
div.className = 'c';
document.body.appendChild(div);
function restore() {
div.innerHTML = '';
const {canvas, gl} = createCanvas(div, () => {
done = true;
group.gl = undefined;
div.innerHTML = "context lost, click to restore";
div.addEventListener('click', restore, {once: true});
});
group.gl = gl;
group.canvas = canvas;
}
restore();
groups.push(group);
}
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function main() {
while (!done) {
createGroup();
await wait();
}
}
function render() {
for (const {gl} of groups) {
if (!gl) {
continue;
}
gl.clearColor(Math.random() * 0.5 + 0.5, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
main();
.c {
display: inline-block;
border: 1px solid black;
margin: 1px;
font-size: xx-small;
}
canvas {
width: 100px;
height: 10px;
display: block;
}
Also note, the original point of WebGL losing the context is that WebGL has no control over the OS. In Windows for example, if any app does something on the GPU that takes too long the OS itself will reset the GPU which effectively loses the contexts for all apps, not just your browser. ALL APPS. There's nothing a browser can do to prevent that and so the browser just has to pass that info down to your webpage. Also in Windows, you can enable/disable GPUs without rebooting. That's another case where the browser has no control and just has to tell you the context was lost.

Related

How to get browsers to release memory used by OfflineAudioContext?

I'm using OfflineAudioContext to render WebAudio signals into a buffer so I can analyze the results. When I do this repeatedly, it seems that the associated memory is never released, eventually causing the browser tab to crash.
Here's a minimal reproduction:
// render 10 minutes of audio into a big buffer
var ctx = new OfflineAudioContext(1, 44100 * 600, 44100)
var osc = ctx.createOscillator()
osc.start()
ctx.startRendering().then(buffer => {
// attempt to clean up
osc.stop()
osc = null
buffer = null
ctx = null
})
Running that in a JS console will render a ~100MB buffer that never gets released. Running it repeatedly will chew up memory until the tab eventually crashes (tested in mac chrome/mozilla, windows chrome/mozilla/edge).
How can I get browsers to free the memory associated with an OfflineAudioContext?
This has been confirmed as a bug, with no workarounds. Until it's fixed, it looks like this is a fact of life.
This one was a major headache, but I finally found a workaround: create an iframe in which you run the audio rendering code, return the result to your main window using postMessage, and immediately remove the iframe from the DOM when you receive the result. That clears all resources associated with it, including the offlineAudioContext.
Of course, that's only practical if your use case is to do a relatively small number of relatively long renders.
Note that to transfer the data back in an efficient manner, you should send ArrayBuffer objects, which are Transferable:
context.oncomplete = function (e) {
var channels = [];
for (var c = 0; c < e.renderedBuffer.numberOfChannels; c++) {
channels.push(e.renderedBuffer.getChannelData(c));
}
channels = channels.map( function(c) {return c.buffer;} );
window.parent.postMessage({
action:'renderResult',
data: channels
}, "*", channels);
}
and recreate Float32Arrays from them on the receiving end.

Detect if console/devtools is open in all browsers

I'm trying to create a script which will run when any browser console is opened or closed. Is there any way to detect if the browser console in all browsers (Firefox/IE/Chrome/Safari/Opera) is open or not via JavaScript, jQuery, or any other client-side script?
If you are willing to accept an interference for the user,
you could use the debugger statement, as it is available in all major browsers.
Side note: If the users of your app are interested in console usage, they're probably familiar with dev tools, and will not be surprised by it showing up.
In short, the statement is acting as a breakpoint, and will affect the UI only if the browser's development tools is on.
Here's an example test:
<body>
<p>Devtools is <span id='test'>off</span></p>
<script>
var minimalUserResponseInMiliseconds = 100;
var before = new Date().getTime();
debugger;
var after = new Date().getTime();
if (after - before > minimalUserResponseInMiliseconds) { // user had to resume the script manually via opened dev tools
document.getElementById('test').innerHTML = 'on';
}
</script>
</body>
devtools-detect should do the job. Try the simple demo page.
devtools-detect → detect whether DevTools is open, and its orientation.
Supported Browsers:
DevTools in Chrome, Safari, Firefox & Opera
Caveats:
Doesn't work if DevTools is undocked (separate window), and may show a false positive if you toggle any kind of sidebar.
I don't think it is directly possible in JS for security reasons.But in here
they are claiming that it is possible and is useful for when you want something special to happen when DevTools is open. Like pausing canvas, adding style debug info, etc.
But As #James Hill tell in this, I also thinks even if a browser chose to make this information accessible to the client, it would not be a standard implementation (supported across multiple browsers).
Also can also try this one also here.
It's not possible in any official cross browser way, but if the occasional false positive is acceptable, you can check for a window.onresize event. Users resizing their windows after loading a page is somewhat uncommon. It's even more effective if you expect users will be frequently opening the console, meaning less false positives as a percentage.
window.onresize = function(){
if ((window.outerHeight - window.innerHeight) > 100) {
// console was opened (or screen was resized)
}
}
Credit to https://stackoverflow.com/a/7809413/3774582. Although that question is chrome specific, the concept applies here.
To expand on this, if you need a very low tolerance on false positives, most window resizes will trigger this event dozens of times because it is usually done as a drag action, while opening the console will only trigger this once. If you can detect this, the approach will become even more accurate.
Note: This will fail to detect if the console is already open when the user visits the page, or if the user opens the console in a new window.
(function() {
'use strict';
const el = new Image();
let consoleIsOpen = false;
let consoleOpened = false;
Object.defineProperty(el, 'id', {
get: () => {
consoleIsOpen = true;
}
});
const verify = () => {
console.dir(el);
if (consoleIsOpen === false && consoleOpened === true) {
// console closed
window.dispatchEvent(new Event('devtools-opened'));
consoleOpened = false;
} else if (consoleIsOpen === true && consoleOpened === false) {
// console opened
window.dispatchEvent(new Event('devtools-closed'));
consoleOpened = true;
}
consoleIsOpen = false;
setTimeout(verify, 1000);
}
verify();
})();
window.addEventListener('devtools-opened', ()=>{console.log('opened')});
window.addEventListener('devtools-closed', ()=>{console.log('closed')});
Here is a code that worked for me.
This solution works like a charm
https://github.com/sindresorhus/devtools-detect
if you are not using modules - disable lines
// if (typeof module !== 'undefined' && module.exports) {
// module.exports = devtools;
// } else {
window.devtools = devtools;
// }
and result is then here
window.devtools.isOpen
I for my project use the blur event.
function yourFunction() {}
window.addEventListener('blur',(e)=>{e.preventDefault(); yourFunction()})
This will execute yourFunction when the window loses focus.
For instance when someone opens the DevTools.
Okay seems like it also fires when you try to access a different window... so maybe not the best solution.
Maybe pair it with looking at the width of the browser.
If it chainged you can be pretty sure about it I think

iPod/iPhone 4, iOS 6.1.6, Safari unexpectedly closes during HTML5/CSS3/JavaScript game initialization

In my HTML5/CSS3/JavaScript application I create instances of the main classes of the game. For example:
function initialize (iconsParamArray) {
for (var i = 0; i < iconsParamArray.length; i++) {
this._iconsArray[i] = new Icon(iconsParamArray[i]);
}
}
The length of the iconsParamArray can be more than one hundred. And sometimes in the middle of this process Safari unexpectedly closes.
If I perform browser cache clearing in Safari settings panel, Safari works normally in around 70% of cases. It looks like GC can't collect all garbage from the memory in time. So memory amount exceeded the limit and browser closes.
Has anyone run into the same problem? How can I avoid this unexpected closings? Maybe I have to use setTimeout function for dividing initialization process on few parts?

In a Firefox restartless add-on, how do I run code when a new window opens (listen for window open)?

I am starting to build a restartless Firefox add-on and I am having trouble setting up the bootstrap.js. Everyone seems to agree that the core of a bootstrap.js is pretty much boilerplate code, along these lines:
const Cc = Components.classes;
const Ci = Components.interfaces;
function startup() {
let wm = Cc["#mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
let windows = wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements()) {
let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
// then can control what happens with domWindow.document
}
}
function shutdown() {}
function install() {}
function uninstall() {}
This code works and I can control things in the existing windows. For example, domWindow.alert("text") successfully creates a standard alert saying "text" on every window that is currently open.
However, I can't find any code that will allow me to do things in new windows; i.e. those created after the script runs. What is the correct way to handle the creation of new windows and gain control over them, to the point where I could get another "text" alert from one when it is created?
Edit: Using the nsWindowMediator class and the code sample from MDN, I now have this:
var windowListener = {
onOpenWindow: function (aWindow) {
try {
let domWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
domWindow.addEventListener("load", function () {
domWindow.removeEventListener("load", arguments.callee, false);
//window has now loaded now do stuff to it
domWindow.alert("text");
}, false);
} catch (err) {
Services.prompt.alert(null, "Error", err);
}
},
onCloseWindow: function (aWindow) {},
onWindowTitleChange: function (aWindow, aTitle) {}
};
function startup(aData, aReason) {
// Load into any existing windows
try {
let wm = Cc["#mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator);
let windows = wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements()) {
let domWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
loadIntoWindow(domWindow);
}
} catch (err) {
Services.prompt.alert(null, "Error", err);
}
Services.wm.addListener(windowListener);
}
However, there is still no output from the onOpenWindow call - the "text" alert does not appear, nor does the error alert in the catch block. I can confirm that onOpenWindow is actually being entered; if I put a Services.prompt.alert() at the beginning of onOpenWindow, I get the alert when I create a new window. Unfortunately, I get an infinite loop of alerts and I have no idea why.
However, I can't find any code that will allow me to do things in new windows; i.e. those created after the script runs. What is the correct way to handle the creation of new windows and gain control over them, to the point where I could get another "text" alert from one when it is created?
The correct way to act on each window when it opens is to use addListener() from nsIWindowMediator. The example code below does this. The nsIWindowMediator is included in Services.jsm and is accessed through Services.wm.addListener(WindowListener). In order to use a window listener, you have to pass it an nsIWindowMediatorListener (ref2) object. An nsIWindowMediatorListener contains three keys: onOpenWindow, onCloseWindow, and onWindowTitleChange. Each should be defined as a function which will be called when the appropriate event occurs.
The MDN document How to convert an overlay extension to restartless in "Step 9: bootstrap.js" contains an example of a basic bootstrap.js which will run the code in the function loadIntoWindow(window) for each currently open browser window and any browser window which opens in the future. I have used code modified from this in a couple of different add-ons. The example is substantially similar to the code you are already using. The example is (slightly modified):
const Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/Services.jsm");
function startup(data,reason) {
// Load this add-ons module(s):
Components.utils.import("chrome://myAddon/content/myModule.jsm");
// Do whatever initial startup stuff is needed for this add-on.
// Code is in module just loaded.
myModule.startup();
// Make changes to the Firefox UI to hook in this add-on
forEachOpenWindow(loadIntoWindow);
// Listen for any windows that open in the future
Services.wm.addListener(WindowListener);
}
function shutdown(data,reason) {
if (reason == APP_SHUTDOWN)
return;
// Unload the UI from each window
forEachOpenWindow(unloadFromWindow);
// Stop listening for new windows to open.
Services.wm.removeListener(WindowListener);
// Do whatever shutdown stuff you need to do on add-on disable
myModule.shutdown();
// Unload the module(s) loaded specific to this extension.
// Use the same URL for your module(s) as when loaded:
Components.utils.unload("chrome://myAddon/content/myModule.jsm");
// HACK WARNING: The Addon Manager does not properly clear all add-on related caches
// on update. In order to fully update images and locales, their
// caches need clearing here.
Services.obs.notifyObservers(null, "chrome-flush-caches", null);
}
function install(data,reason) { }
function uninstall(data,reason) { }
function loadIntoWindow(window) {
/* call/move your UI construction function here */
}
function unloadFromWindow(window) {
/* call/move your UI tear down function here */
}
function forEachOpenWindow(todo) {
// Apply a function to all open browser windows
var windows = Services.wm.getEnumerator("navigator:browser");
while (windows.hasMoreElements())
todo(windows.getNext().QueryInterface(Ci.nsIDOMWindow));
}
var WindowListener = {
onOpenWindow: function(xulWindow) {
var window = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow);
function onWindowLoad() {
window.removeEventListener("load",onWindowLoad);
// Only add UI changes if this is a browser window
if (window.document.documentElement.getAttribute("windowtype")
== "navigator:browser")
loadIntoWindow(window);
}
window.addEventListener("load",onWindowLoad);
},
onCloseWindow: function(xulWindow) { },
onWindowTitleChange: function(xulWindow, newTitle) { }
};
While there is quite a bit more that your might want to do in your bootstrap.js code, the above is organized reasonably well and keeps all of the code to load into the Firefox UI within loadIntoWindow(window) and unloading the UI within unloadFromWindow(window). However, it should be noted that some UI elements you should only be adding/removing once (e.g. australis widgets, like buttons) and other elements (e.g. direct changes to the Firefox DOM) have to be added once in each window.
Unfortunately, I get an infinite loop of alerts and I have no idea why.
One of the significant differences between this example and what you are currently using is the test for the type of window that has opened. This is done so that we are only acting on newly opened windows which are browser windows instead of all newly opened windows:
if (window.document.documentElement.getAttribute("windowtype") == "navigator:browser")
loadIntoWindow(window);
The problem you describe of getting an infinite loop of alert() popups is caused by not checking to make sure that you are only acting on browser windows. The alert() popup is a window. Thus, you are calling alert() for every alert() window you open which, of course, just opens another alert() window on which you call alert(). This is your infinite loop.
Additional references:
1. Working with windows in chrome code
However, I can't find any code that will allow me to do things in new windows
When working with XPCOM objects you generally want to study their interfaces, which are often found on MDN. In this case your starting point would be nsIWindowMediator, since that's the service you're using in line 5.
As you can see it has an addListener function, which takes a parameter implementing nsIWindowMediatorListener. There's a code-example right there on the page.
But let's assume for the moment there isn't a code example. You could search for the interface on MDN, but it isn't listed. The next step would be searching MXR for the .idl. idl = interface description language
Once you got the interface contract you can more or less just implement it in javascript, at least for listeners. Implementing your own xpcom services would be a little more complicated.
Searching the addon sdk can often provide some hints too. In this case they don't seem to be using .addListener, but the file hints at another interesting service, which in turn you can find on MDN: nsIWindowWatcher.
Basically, if you're writing restartless addons you're rummaging through the entrails of firefox and will have to do some detective work to find the exact components you need. If you want something more convenient I would recommend the addon sdk, which provides a more organized but also more restricted set of commonly used APIs

Bring spell check window to foreground with JavaScript/JScript in Windows 7

I have some JScript code (converted from some old VBScript) that starts like this:
var Word = new ActiveXObject("Word.Basic");
Word.FileNew(); // opens new Word document
Word.Insert(IncorrectText);
Word.ToolsSpelling(); // opens spell check behind IE
The idea is to utilize the MS Word spell check for browser use, and it works well in XP, but the spell check box opens in the background in Windows 7 / IE 8 (this question tells me that the problem started in Vista and is probably an OS issue, not a browser or Office issue).
So my question is, how can I bring this window to the foreground? One important note is that the last line, Word.ToolsSpelling();, locks up my script, so anything I do will need to be before that.
I've tried
var wshShell = new ActiveXObject("WScript.Shell");
wshShell.AppActivate("Document1 - Microsoft Word"); // or some other text
before the ToolsSpelling call, but this does not do anything (maybe because the Word document is not actually revealed at this point?). Of course, this will only work if no "Document1" is already opened, so this is a questionable thought to begin with.
Per this answer, I tried using window.blur(); in order to blur IE, but this will only work if the IE window is the only one opened. Maybe there's some way I can loop through all opened windows and apply this?
SetForegroundWindow looked promising, but I don't know how to use it in JSript.
Any ideas?
Note: Browser permissions will be completely open for this site.
Update: Turns out if you use Word.Application, the spell check comes up in front as it should. Only the Word.Basic method has this problem (I don't expect to know why this side of eternity):
var wordApp = new ActiveXObject("Word.Application");
wordApp.Documents.Add();
wordDoc = wordApp.ActiveDocument;
... // safety checks before insertion
wordSelection.TypeText(IncorrectText);
wordDoc.CheckSpelling();
wordApp.Visible = false; // CheckSpelling makes the document visible
You might be able to jigger the window state. When the window is maximized after having been minimized, Windows will stack that in front (zIndex to top).
Something like:
var WIN_MAX = 2;
var WIN_MIN = 1;
var Word = new ActiveXObject("Word.Application");
Word.Visible = true;
// minimize the app
Word.WindowState = WIN_MIN ;
// in 500ms, maximize
setTimeout(function () {
Word.WindowState = WIN_MAX;
}, 500);
The setTimeout call seeks to work around a timing issue; Windows will sometimes "get confused" when a programmatic minimize/maximize happens in immediate succession. You might have to extend that delay a little, test it repeatedly and see what kind of results you get.

Categories