how to bypass the setTimeout throttle in a hidden tab? - javascript

I'm am doing a heavy "scientific" (ie, not displaying data) webgl computation. Webgl can't be put in a worker, and doing a lot of webgl blocks the whole browser so I sliced my computation in chunks, and I compute each chunk in a setTimeout() function (after calling getError() to flush the opengl queue). I leave a bit of time in between the chunks so that the browser has time to flush some UI events from the main UI queue and it makes the whole thing feel a bit less sluggish.
My problem is that when the tab is hidden, the setTimeout gets throttled to a one second period which is way too slow for me.
Is there a better solution than what I did? Obviously requestAnimationFrame() doesn't work, since it's never called back in hidden tabs (and it's too slow in visible).
Is there a non-throttled time event in the hidden state? I tried to use window.postMessage() but it's still too fast and the whole browser feels slow.
here is the current state of my research:
function drawTile(sequenceIndex) {
if (sequenceIndex < sequence.length) {
var x = sequence[sequenceIndex][0];
var y = sequence[sequenceIndex][1];
setTilePos(x, y);
modelStage.render(renderer, modelBuffer);
minkowskiPass.render(renderer, minkowskiBuffer, modelBuffer);
copyPass.quad.position.x = x;
copyPass.quad.position.y = y;
copyPass.render(renderer, null, minkowskiBuffer);
var gl = renderer.getContext();
gl.getError();
sequenceIndex++;
if (document.visibilityState != "hidden") {
setTimeout(function () {
drawTile(sequenceIndex);
}, 10);
} else {
//window.postMessage is not rate limited then the tab is hidden
// we need to slow the computation by an event, otherwise the whole browser is unresponsive.
$(window).one('message', function () {
drawTile(sequenceIndex);
});
window.postMessage('lol', '*');
}
} else
console.timeEnd('computation');
}
console.time('computation');
drawTile(0);

Here's another convoluted workaround for anyone who needs it; you can use the Web Audio API to generate function calls:
var setTimeout2 = (function () {
var samples = 2048;
var fns = [];
var context = new AudioContext();
var source = context.createBufferSource();
var node = context.createScriptProcessor(samples, 1, 1);
// This gets fired every ~46 milliseconds. You can change
// `samples` to another valid value (256, 512, 1024, 2048,
// 4096, 8192, or 16384); then it'll get called every
// `samples / context.sampleRate` seconds (~46 ms for
// `samples == 2048` and `context.sampleRate == 44100`).
node.onaudioprocess = function (e) {
fns = fns.filter(function (fn) {
return !fn(Date.now() - fn.t);
});
};
source.connect(node);
node.connect(context.destination);
window.do_not_garbage_collect = [context, source, node];
return function (fn) {
fn.t = Date.now();
fns.push(fn);
};
}());
// Use like this:
setTimeout2(function (t) {
console.log(t);
// End after 1 second.
if (t > 1000)
return true;
})

Perhaps have a worker thread also run a
postMessage loop and a fraction of the time (every n iterations), either pause or resume the main thread?

Related

Cannot record HTMLCanvasElement when tab is blurred even though render is occuring [duplicate]

When using CanvasCaptureMediaStream and MediaRecorder, is there a way to get an event on each frame?
What I need is not unlike requestAnimationFrame(), but I need it for the CanvasCaptureMediaStream (and/or the MediaRecorder) and not the window. The MediaRecorder could be running at a different frame rate than the window (possibly at a not regularly divisible rate, such as 25 FPS vs 60 FPS), so I want to update the canvas at its frame rate rather than the window's.
This example currently only fully works on FireFox, since chrome simply stops the canvas stream when the tab is blurred... (probably related to this bug, but well, my timer seems to be working but not the recording...)
[Edit]: it actually now works only in chrome, since they have fixed this bug, but not anymore in FF because of this one (caused by e10s).
There doesn't seem to be any event on MediaStream letting you know when a frame has been rendered to it, neither on the MediaRecorder.
Even the currentTime property of the MediaStream (currently only available in FF) doesn't seem to be changing accordingly with the fps argument passed in the captureStream() method.
But what you seem to want is a reliable timer, that won't loose its frequency when i.e the current tab is not focused (which happens for rAF).
Fortunately, the WebAudio API does also have an high precision timer, based on hardware clock, rather than on screen refresh rate.
So we can come with an alternative timed loop, able to keep its frequency even when the tab is blurred.
/*
An alternative timing loop, based on AudioContext's clock
#arg callback : a callback function
with the audioContext's currentTime passed as unique argument
#arg frequency : float in ms;
#returns : a stop function
*/
function audioTimerLoop(callback, frequency) {
// AudioContext time parameters are in seconds
var freq = frequency / 1000;
var aCtx = new AudioContext();
// Chrome needs our oscillator node to be attached to the destination
// So we create a silent Gain Node
var silence = aCtx.createGain();
silence.gain.value = 0;
silence.connect(aCtx.destination);
onOSCend();
var stopped = false;
function onOSCend() {
osc = aCtx.createOscillator();
osc.onended = onOSCend;
osc.connect(silence);
osc.start(0);
osc.stop(aCtx.currentTime + freq);
callback(aCtx.currentTime);
if (stopped) {
osc.onended = function() {
return;
};
}
};
// return a function to stop our loop
return function() {
stopped = true;
};
}
function start() {
// start our loop #25fps
var stopAnim = audioTimerLoop(anim, 1000 / 25);
// maximum stream rate set as 25 fps
cStream = canvas.captureStream(25);
let chunks = [];
var recorder = new MediaRecorder(cStream);
recorder.ondataavailable = e => chunks.push(e.data);
recorder.onstop = e => {
// we can stop our loop
stopAnim();
var url = URL.createObjectURL(new Blob(chunks));
var v = document.createElement('video');
v.src = url;
v.controls = true;
document.body.appendChild(v);
}
recorder.start();
// stops the recorder in 20s, try to change tab during this time
setTimeout(function() {
recorder.stop();
}, 20000)
}
// make something move on the canvas
var ctx = canvas.getContext('2d');
var x = 0;
function anim() {
x = (x + 2) % (canvas.width + 100);
ctx.fillStyle = 'ivory';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'red';
ctx.fillRect(x - 50, 20, 50, 50)
};
btn.onclick = start;
<button id="btn">begin</button>
<canvas id="canvas" width="500" height="200"></canvas>
Nota Bene :
In this example, I set the frequency to 25fps, but we can set it to 60fps and it seems to work correctly even on my old notebook, at least with such a simple animation.

Exact time of display: requestAnimationFrame usage and timeline

What I want to achieve is to detect the precise time of when a certain change appeared on the screen (primarily with Google Chrome). For example I show an item using $("xelement").show(); or change it using $("#xelement").text("sth new"); and then I would want to see what the performance.now() was exactly when the change appeared on the user's screen with the given screen repaint. So I'm totally open to any solutions - below I just refer primarily to requestAnimationFrame (rAF) because that is the function that is supposed to help achieve exactly this, only it doesn't seem to; see below.
Basically, as I imagine, rAF should execute everything inside it in about 0-17 ms (whenever the next frame appears on my standard 60 Hz screen). Moreover, the timestamp argument should give the value of the time of this execution (and this value is based on the same DOMHighResTimeStamp measure as performance.now()).
Now here is one of the many tests I made for this: https://jsfiddle.net/gasparl/k5nx7zvh/31/
function item_display() {
var before = performance.now();
requestAnimationFrame(function(timest){
var r_start = performance.now();
var r_ts = timest;
console.log("before:", before);
console.log("RAF callback start:", r_start);
console.log("RAF stamp:", r_ts);
console.log("before vs. RAF callback start:", r_start - before);
console.log("before vs. RAF stamp:", r_ts - before);
console.log("")
});
}
setInterval(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
What I see in Chrome is: the function inside rAF is executed always within about 0-3 ms (counting from a performance.now() immediately before it), and, what's weirdest, the rAF timestamp is something totally different from what I get with the performance.now() inside the rAF, being usually about 0-17 ms earlier than the performance.now() called before the rAF (but sometimes about 0-1 ms afterwards).
Here is a typical example:
before: 409265.00000001397
RAF callback start: 409266.30000001758
RAF stamp: 409260.832
before vs. RAF callback start: 1.30000000353902
before vs. RAF stamp: -4.168000013974961
In Firefox and in IE it is different. In Firefox the "before vs. RAF callback start" is either around 1-3 ms or around 16-17 ms. The "before vs. RAF stamp" is always positive, usually around 0-3 ms, but sometimes anything between 3-17 ms. In IE both differences are almost always around 15-18 ms (positive). These are more or less the same of different PCs. However, when I run it on my phone's Chrome, then, and only then, it seems plausibly correct: "before vs. RAF stamp" randomly around 0-17, and "RAF callback start" always a few ms afterwards.
For more context: This is for an online response-time experiment where users use their own PC (but I typically restrict browser to Chrome, so that's the only browser that really matters to me). I show various items repeatedly, and measure the response time as "from the moment of the display of the element (when the person sees it) to the moment when they press a key", and count an average from the recorded response times for specific items, and then check the difference between certain item types. This also means that it doesn't matter much if the recorded time is always a bit skewed in a direction (e.g. always 3 ms before the actual appearance of the element) as long as this skew is consistent for each display, because only the difference really matters. A 1-2 ms precision would be the ideal, but anything that mitigates the random "refresh rate noise" (0-17 ms) would be nice.
I also gave a try to jQuery.show() callback, but it does not take refresh rate into account: https://jsfiddle.net/gasparl/k5nx7zvh/67/
var r_start;
function shown() {
r_start = performance.now();
}
function item_display() {
var before = performance.now();
$("#stim_id").show(complete = shown())
var after = performance.now();
var text = "before: " + before + "<br>callback RT: " + r_start + "<br>after: " + after + "<br>before vs. callback: " + (r_start - before) + "<br>before vs. after: " + (after - r_start)
console.log("")
console.log(text)
$("p").html(text);
setTimeout(function(){ $("#stim_id").hide(); }, 500);
}
setInterval(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 800);
With HTML:
<p><br><br><br><br><br></p>
<span id="stim_id">STIMULUS</span>
The solution (based on Kaiido's answer) along with working display example:
function monkeyPatchRequestPostAnimationFrame() {
const channel = new MessageChannel();
const callbacks = [];
let timestamp = 0;
let called = false;
channel.port2.onmessage = e => {
called = false;
const toCall = callbacks.slice();
callbacks.length = 0;
toCall.forEach(fn => {
try {
fn(timestamp);
} catch (e) {}
});
};
window.requestPostAnimationFrame = function(callback) {
if (typeof callback !== 'function') {
throw new TypeError('Argument 1 is not callable');
}
callbacks.push(callback);
if (!called) {
requestAnimationFrame((time) => {
timestamp = time;
channel.port1.postMessage('');
});
called = true;
}
};
}
if (typeof requestPostAnimationFrame !== 'function') {
monkeyPatchRequestPostAnimationFrame();
}
function chromeWorkaroundLoop() {
if (needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
}
// here is how I display items
// includes a 100 ms "warm-up"
function item_display() {
window.needed = true;
chromeWorkaroundLoop();
setTimeout(function() {
var before = performance.now();
$("#stim_id").text("Random new text: " + Math.round(Math.random()*1000) + ".");
$("#stim_id").show();
// I ask for display above, and get display time below
requestPostAnimationFrame(function() {
var rPAF_now = performance.now();
console.log("before vs. rPAF now:", rPAF_now - before);
console.log("");
needed = false;
});
}, 100);
}
// below is just running example instances of displaying stuff
function example_loop(count) {
$("#stim_id").hide();
setTimeout(function() {
item_display();
if (count > 1) {
example_loop(--count);
}
}, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
}
example_loop(10);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
<div id="stim_id">Any text</div>
EDIT: So, based on empirical measurements, out of all this, it turns out that all that matters is the rAF loop. The rPAF makes no real difference.
What you are experiencing is a Chrome bug (and even two).
Basically, when the pool of requestAnimationFrame callbacks is empty, they'll call it directly at the end of the current event loop, without waiting for the actual painting frame as the specs require.
To workaround this bug, you can keep an ever-going requestAnimationFrame loop, but beware this will mark your document as "animated" and will trigger a bunch of side-effects on your page (like forcing a repaint at every screen refresh). So I'm not sure what you are doing, but it's generally not a great idea to do this, and I would rather invite you to run this animation loop only when required.
let needed = true; // set to false when you don't need the rAF loop anymore
function item_display() {
var before = performance.now();
requestAnimationFrame(function(timest) {
var r_start = performance.now();
var r_ts = timest;
console.log("before:", before);
console.log("RAF callback start:", r_start);
console.log("RAF stamp:", r_ts);
console.log("before vs. RAF callback start:", r_start - before);
console.log("before vs. RAF stamp:", r_ts - before);
console.log("")
setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
});
}
chromeWorkaroundLoop();
item_display();
function chromeWorkaroundLoop() {
if (needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
};
Now, requestAnimationFrame callbacks fire before the next paint (actually in the same event loop), and the TimeStamp argument should represent the time after all main tasks and microtasks of the current frame were executed, before it's starts its "update the rendering" sub-task (step 9 here).
[edit]: However it's not really what browsers implement, see this Q/A for more details.
So it's not the most precise you can have, and you are right that using performance.now() inside this callback should get you closer to the actual painting time.
Moreover when Chrome faces yet an other bug here, probably related to the first one, when they do set this rAF timeStamp to... I must admit I don't know what... maybe the previous painting frame's timeStamp.
(function() {
let raf_id,
eventLoopReport = {
id: 0,
timeStamp: 0,
now: 0
},
report = {
nb_of_loops_between_call_and_start: -1,
mouseClick_timeStamp: 0,
calling_task: {
eventLoop: null,
now: 0
},
rAF_task: {
eventLoop: null,
now: 0,
timeStamp: 0
}
};
startEventLoopCounter();
btn.onclick = triggerSingleFrame;
// increments eventLoop_id at every event loop
// (or at least every time our postMessage loop fires)
function startEventLoopCounter() {
const channel = new MessageChannel()
channel.port2.onmessage = e => {
eventLoopReport.id ++;
eventLoopReport.timeStamp = e.timeStamp;
eventLoopReport.now = performance.now();
channel.port1.postMessage('*');
};
channel.port1.postMessage('*');
}
function triggerSingleFrame(e) {
// mouseClick Event should be generated at least the previous event loop, so its timeStamp should be in the past
report.mouseClick_timeStamp = e.timeStamp;
const report_calling = report.calling_task;
report_calling.now = performance.now();
report_calling.eventLoop = Object.assign({}, eventLoopReport);
cancelAnimationFrame(raf_id);
raf_id = requestAnimationFrame((raf_ts) => {
const report_rAF = report.rAF_task;
report_rAF.now = performance.now();
report_rAF.timeStamp = raf_ts;
report_rAF.eventLoop = Object.assign({}, eventLoopReport);
report.nb_of_loops_between_call_and_start = report_rAF.eventLoop.id - report_calling.eventLoop.id;
// this should always be positive
report_el.textContent = "rAF.timeStamp - mouse_click.timeStamp: " +
(report.rAF_task.timeStamp - report.mouseClick_timeStamp) + '\n\n' +
// verbose
JSON.stringify(report, null, 2) ;
});
}
})();
<button id="btn">flash</button>
<div id="out"></div>
<pre id="report_el"></pre>
Once again, running an infinite rAF loop will fix this weird bug.
So one thing you might want to check is the maybe incoming requestPostAnimationFrame method.
You can access it in Chrome,1 after you enable "Experimental Web Platform features" in chrome:flags. This method if accepted by html standards will allow us to fire callbacks immediately after the paint operation occurred.
From there, you should be at the closest of the painting.
var needed = true;
function item_display() {
var before = performance.now();
requestAnimationFrame(function() {
requestPostAnimationFrame(function() {
var rPAF_now = performance.now();
console.log("before vs. rPAF now:", rPAF_now - before);
console.log("");
setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
});
});
}
if (typeof requestPostAnimationFrame === 'function') {
chromeWorkaroundLoop();
item_display();
} else {
console.error("Your browser doesn't support 'requestPostAnimationFrame' method, be sure you enabled 'Experimental Web Platform features' in chrome:flags");
}
function chromeWorkaroundLoop() {
if (needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
};
And for the browsers that do not yet implement this proposal, or if this proposal never does it through the specs, you can try to polyfill it using a MessageEvent, which should be the first thing to fire at the next event loop.
// polyfills requestPostAnimationFrame
// requestPostAnimationFrame polyfill
if (typeof requestPostAnimationFrame !== "function") {
(() => {
const channel = new MessageChannel();
const callbacks = [];
let timestamp = 0;
let called = false;
let scheduled = false; // to make it work from rAF
let inRAF = false; // to make it work from rAF
channel.port2.onmessage = e => {
called = false;
const toCall = callbacks.slice();
callbacks.length = 0;
toCall.forEach(fn => {
try {
fn(timestamp);
} catch (e) {}
});
}
// We need to overwrite rAF to let us know we are inside an rAF callback
// as to avoid scheduling yet an other rAF, which would be one painting frame late
// We could have hooked an infinite loop on rAF, but this means
// forcing the document to be animated all the time
// which is bad for perfs
const rAF = globalThis.requestAnimationFrame;
globalThis.requestAnimationFrame = function(...args) {
if (!scheduled) {
scheduled = true;
rAF.call(globalThis, (time) => inRAF = time);
globalThis.requestPostAnimationFrame(() => {
scheduled = false;
inRAF = false;
});
}
rAF.apply(globalThis, args);
};
globalThis.requestPostAnimationFrame = function(callback) {
if (typeof callback !== "function") {
throw new TypeError("Argument 1 is not callable");
}
callbacks.push(callback);
if (!called) {
if (inRAF) {
timestamp = inRAF;
channel.port1.postMessage("");
} else {
requestAnimationFrame((time) => {
timestamp = time;
channel.port1.postMessage("");
});
}
called = true;
}
};
})();
}
var needed = true;
function item_display() {
var before = performance.now();
requestPostAnimationFrame(function() {
var rPAF_now = performance.now();
console.log("before vs. rPAF now:", rPAF_now - before);
console.log("");
setTimeout(item_display, Math.floor(Math.random() * (1000 - 500 + 1)) + 500);
});
}
chromeWorkaroundLoop();
item_display();
function chromeWorkaroundLoop() {
if (needed) {
requestAnimationFrame(chromeWorkaroundLoop);
}
};
Turns out this feature has apparently been removed from Chrome experiments. Looking at the implementation issue I can't find why, when, nor if they plan to still work on it.

Javascript - process array in batches AND show progress

Dear Javascript Guru's:
I have the following requirements:
Process a large array in batches of 1000 (or any arbitrary size).
When each batch is processed, update the UI to show our progress.
When all batches have been processed, continue with the next step.
For example:
function process_array(batch_size) {
var da_len = data_array.length;
var idx = 0;
function process_batch() {
var idx_end = Math.min(da_len, idx + batch_size);
while (idx < idx_end) {
// do the voodoo we need to do
}
}
// This loop kills the browser ...
while (idx < da_len) {
setTimeout(process_batch, 10);
// Show some progress (no luck) ...
show_progress(idx);
}
}
// Process array ...
process_array(1000);
// Continue with next task ...
// BUT NOT UNTIL WE HAVE FINISHED PROCESSING THE ARRAY!!!
Since I am new to javascript, I discovered that everything is done on a single thread and as such, one needs to get a little creative with regard to processing and updating the UI. I have found some examples using recursive setTimeout calls, (one key difference is I have to wait until the array has been fully processed before continuing), but I cannot seem to get things working as described above.
Also -- I am in need of a "pure" javascript solution -- no third party libraries or the use of web workers (that are not fully supported).
Any (and all) guidance would be appreciated.
Thanks in advance.
You can make a stream from array and use batch-stream to make batches so that you can stream in batches to UI.
stream-array
and
batch-stream
In JavaScript when executing scripts in a HTML page, the page becomes unresponsive until the script is finished. This is because JavaScript is single thread.
You could consider using a web worker in JavaScript that runs in the background, independently of other scripts, without affecting the performance of the page.
In this case User can continue to do whatever he wants in the UI.
You can send and receive messages from the web worker.
More info on Web Worker here.
So part of the magic of recursion is really thinking about the things that you need to pass in, to make it work.
And in JS (and other functional languages) that frequently involves functions.
function processBatch (remaining, processed, batchSize,
transform, onComplete, onProgress) {
if (!remaining.length) {
return onComplete(processed);
}
const batch = remaining.slice(0, batchSize);
const tail = remaining.slice(batchSize);
const totalProcessed = processed.concat(batch.map(transform));
return scheduleBatch(tail, totalProcessed, batchSize,
transform, onComplete, onProgress);
}
function scheduleBatch (remaining, processed, batchSize,
transform, onComplete, onProgress) {
onProgress(processed, remaining, batchSize);
setTimeout(() => processBatch(remaining, processed, batchSize,
transform, onComplete, onProgress));
}
const noop = () => {};
const identity = x => x;
function processArray (array, batchSize, transform, onComplete, onProgress) {
scheduleBatch(
array,
[],
batchSize,
transform || identity,
onComplete || noop,
onProgress || noop
);
}
This can be simplified extremely, and the reality is that I'm just having a little fun here, but if you follow the trail, you should see recursion in a closed system that works with an arbitrary transform, on arbitrary objects, of arbitrary array lengths, with arbitrary code-execution when complete, and when each batch is completed and scheduling the next run.
To be honest, you could even swap this implementation out for a custom scheduler, by changing 3 lines of code or so, and then you could log whatever you wanted...
const numbers = [1, 2, 3, 4, 5, 6];
const batchSize = 2;
const showWhenDone = numbers => console.log(`Done with: ${numbers}`);
const showProgress = (processed, remaining) =>
`${processed.length} done; ${remaining.length} to go`;
const quintuple = x => x * 5;
processArray(
numbers,
batchSize,
quintuple,
showWhenDone,
showProgress
);
// 0 done; 6 to go
// 2 done; 4 to go
// 4 done; 2 to go
// Done with: 5, 10, 15, 20, 25, 30
Overkill? Oh yes. But worth familiarizing yourself with the concepts, if you're going to spend some time in the language.
Thank-you all for your comments and suggestions.
Below is a code that I settled on. The code works for any task (in my case, processing an array) and gives the browser time to update the UI if need be.
The "do_task" function starts an anonymous function via setInterval that alternates between two steps -- processing the array in batches and showing the progress, this continues until all elements in the array have been processed.
function do_task() {
const k_task_process_array = 1;
const k_task_show_progress = 2;
var working = false;
var task_step = k_task_process_array;
var batch_size = 1000;
var idx = 0;
var idx_end = 0;
var da_len = data_array.length;
// Start the task ...
var task_id = setInterval(function () {
if (!working) {
working = true;
switch (task_step) {
case k_task_process_array:
idx_end = Math.min( idx + batch_size, da_len );
while (idx < idx_end) {
// do the voodoo we need to do ...
}
idx++;
}
task_step = k_task_show_progress;
working = false;
break;
default:
// Show progress here ...
// Continue processing array ...
task_step = k_task_process_array;
working = false;
}
// Check if done ...
if (idx >= da_len) {
clearInterval(task_id);
task_id = null;
}
working = false;
}
}, 1);
}

Rate limiting to prevent malicious behavior in ExpressJS

Someone made me aware of some flaws in an application I'm working on (mostly within my JavaScript on the front-end), that leaves open the possibility of, say, clicking a ton of buttons at once and sending out a ton of transactional emails. This is clearly not good.
I think one way to handle this in ExpressJS is by using app.all() to count the number of requests that happen within a certain timeframe. I'd store this in the session metadata with timestamps, and if more than X requests happen in Y time, I cut them off for awhile until the limit expires.
Has anyone done this before or have any tips/hints to help me out? Something that's easy to drop in and out of my app is preferable. Thanks!
You could use the Collate object in your webpage.
function Collate(timeout) {
this.timeout = timeout || 1000;
}
Collate.prototype = {
time: 0,
idle: function() {
var t = new Date().getTime();
return (t - this.time > this.timeout && (this.time = t));
},
prefer: function(func) {
this.func = func;
clearTimeout(this.timer);
this.timer = setTimeout(func, this.timeout);
}
};
If you want a function to run once and not run again within the next 1 second.
Like if you want to prevent the user from submitting a form many times, you do this:
var timer = new Collate(3000); //3 seconds
button1.onclick = function() {
if(timer.idle()) {
button1.form.submit();
} else alert("Don't click too quickly!");
}
//or on the form tag
<script>var submitTimer = new Collate(3000);</script>
<form action="post" onsubmit="return submitTimer.idle();">
If you expect an event to fire multiple times and only want to react to the last time it fires.
Like if you want to search after a user has finished typing, you do this:
var timer = new Collate(700); //0.7 seconds
textfield1.onkeyup = function() {
timer.prefer(function() {
autocomplete.search(textfield1.value);
});
};

setInterval pauses in Android Browser / Mobile Safari when screen times out

I've built a simple JavaScript-based timer for a mobile webapp; for the sake of example:
var a = 0;
setInterval(function() {
console.log('a', a);
a++;
}, 1000);
This runs just fine in both Mobile Safari and Android Browser. It will log to console every second and increment the value of a accordingly. (Okay, Android Browser doesn't have console.log support, but let's assume it does.)
The issue: if the screen times out (i.e. user stopped interacting with the page), the setInterval function pauses. It resumes when the user turns on their screen again. This won't work for me as I need timer to keep running.
The questions: Is there a way to prevent the setInterval function from pausing when the screen times out? If not, is it possible to prevent the screen from timing out? Any other alternatives?
Thanks in advance!
Basically, no. The phone enters a sleep state to save battery when the screen times out. Since you can't see anything anyway, a large number of processing tasks are stopped. Similar things will occur when you change tabs/windows (the page is unloaded from memory). Right now there is no way to request that the device stays on from a web application. Future support in Android for accessing hardware may provide this functionality, but personally I doubt it.
If you need always running support, you'll need to write native applications for both systems (plus on Android it can always run).
You can use the Page Visibility API to detect when the page is hidden or visible. For example, if the user navigates away from the browser and back again or the screen turns off and on.
I used this answer to help create by solution.
You will need to store the time you set your interval. Then when the visibilityChange event listener indicates the document is visible again, you can calculate the amount of time that has passed since you first started the interval and update your data as needed.
In my case I was creating a count down timer in my Angular2 project. My page was running on an iPad and the timer was pausing whenever the screen turned off. So I added the event listener in my ngOnInit(). Then when the screen turned back on I could update my timer to show the correct time left since it was started.
I am using the moment npm package to handle my date time.
The timerInfo object is a class variable that gets updated by the interval callback. self.zone.run() is used to propagate the changes to the DOM so that the updated time gets displayed.
Written in typescript:
private timerInfo:{
days?:number,
hours?:number,
minutes:number,
seconds:number
};
private startTime:Moment = moment();
private timerDuration:number = 20; // in minutes
private timerHandle:any;
ngOnInit() {
this.setVisibilityListener();
}
private setVisibilityListener():void {
var self = this;
var hidden, visibilityState, visibilityChange;
if (typeof document.hidden !== "undefined") {
hidden = "hidden";
visibilityChange = "visibilitychange";
visibilityState = "visibilityState";
}
var document_hidden = document[hidden];
document.addEventListener(visibilityChange, function () {
if (document_hidden != document[hidden]) {
if (document[hidden]) {
// Document hidden
console.log("document hidden");
} else {
// Document shown
console.log("document shown; setCountDownTimer()");
self.setCountDownTimer();
}
document_hidden = document[hidden];
}
});
}
private setCountDownTimer():void {
var self = this;
if (self.startTime) {
var startMoment = moment(self.startTime);
var endMoment = startMoment.add(self.timerDuration, "minutes");
console.log("endMoment: ", endMoment.toISOString());
self.clearTimer();
var eventTime = endMoment.unix();
var currentTime = moment().unix();
var diffTime = eventTime - currentTime;
var duration = moment.duration(diffTime * 1000, 'milliseconds');
var interval = 1000;
// if time to countdown
if (diffTime > 0) {
self.timerHandle = setInterval(() => {
self.zone.run(() => {
var diff = duration.asMilliseconds() - interval;
if (diff < 0) {
self.clearTimer();
self.timerComplete();
} else {
duration = moment.duration(duration.asMilliseconds() - interval, 'milliseconds');
self.timerInfo = {
days: moment.duration(duration).days(),
hours: moment.duration(duration).hours(),
minutes: moment.duration(duration).minutes(),
seconds: moment.duration(duration).seconds()
};
// console.log("timerInfo: ", JSON.stringify(self.timerInfo));
}
});
}, 1000);
} else {
self.timerComplete();
}
}
}
private clearTimer():void {
if (this.timerHandle) {
clearInterval(this.timerHandle);
this.timerHandle = null;
}
}

Categories

Resources