Skip to content

Instantly share code, notes, and snippets.

@brian-mann
Created August 1, 2017 05:06
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brian-mann/94429cba31ec3a17b08649072bf15578 to your computer and use it in GitHub Desktop.
Save brian-mann/94429cba31ec3a17b08649072bf15578 to your computer and use it in GitHub Desktop.
Fixed timers in electron
process.on('message', (obj = {}) => {
const { id, ms } = obj
setTimeout(() => {
try {
// process.send could throw if
// parent process has already exited
process.send({
id,
ms,
})
} catch (err) {
// eslint-disable no-empty
}
}, ms)
})
// electron has completely busted timers resulting in
// all kinds of bizarre timeouts and unresponsive UI
// https://github.com/electron/electron/issues/7079
//
// this fixes this problem by replacing all the global
// timers and implementing a lightweight queuing mechanism
// involving a forked process
const cp = require('child_process')
const path = require('path')
const log = require('debug')('cypress:server:timers')
const st = global.setTimeout
const si = global.setInterval
const ct = global.clearTimeout
const ci = global.clearInterval
let child = null
function noop () {}
function restore () {
// restore
global.setTimeout = st
global.setInterval = si
global.clearTimeout = ct
global.clearInterval = ci
if (child) {
child.kill()
}
child = null
}
function fix () {
const queue = {}
let idCounter = 0
function sendAndQueue (id, cb, ms, args) {
// const started = Date.now()
log('queuing timer id %d after %d ms', id, ms)
queue[id] = {
// started,
args,
ms,
cb,
}
child.send({
id,
ms,
})
// return the timer object
return {
id,
ref: noop,
unref: noop,
}
}
function clear (id) {
log('clearing timer id %d from queue %o', id, queue)
delete queue[id]
}
// fork the child process
let child = cp.fork(path.join(__dirname, 'child.js'), [], {
stdio: 'inherit',
env: {
ELECTRON_RUN_AS_NODE: true,
},
})
.on('message', (obj = {}) => {
const { id } = obj
const msg = queue[id]
// if we didn't get a msg
// that means we must have
// cleared the timeout already
if (!msg) {
return
}
const { cb, args } = msg
clear(id)
cb(...args)
})
global.setTimeout = function (cb, ms, ...args) {
idCounter += 1
return sendAndQueue(idCounter, cb, ms, args)
}
global.clearTimeout = function (timer) {
if (!timer) {
return
}
// return undefined per the spec
clear(timer.id)
}
global.clearInterval = function (timer) {
if (!timer) {
return
}
// return undefined per the spec
clear(timer.id)
}
global.setInterval = function (fn, ms, ...args) {
const permId = idCounter += 1
function cb () {
// we want to immediately poll again
// because our permId was just cleared
// from the queue stack
poll()
fn()
}
function poll () {
return sendAndQueue(permId, cb, ms, args)
}
return poll()
}
return {
child,
queue,
}
}
module.exports = {
restore,
fix,
}
require("../spec_helper")
_ = require("lodash")
parent = require("#{root}timers/parent")
describe "timers/parent", ->
context ".fix", ->
beforeEach ->
parent.restore()
@timer = parent.fix()
describe "setTimeout", ->
it "returns timer object", (done) ->
obj = setTimeout(done, 10)
expect(obj.id).to.eq(1)
expect(obj.ref).to.be.a("function")
expect(obj.unref).to.be.a("function")
it "increments timer id", (done) ->
fn = _.after(2, done)
obj1 = setTimeout(fn, 10)
obj2 = setTimeout(fn, 10)
expect(obj2.id).to.eq(2)
it "slices out of queue once cb is invoked", (done) ->
fn = =>
expect(@timer.queue).to.deep.eq({})
done()
setTimeout(fn, 10)
expect(@timer.queue[1].cb).to.eq(fn)
describe "clearTimeout", ->
it "does not explode when passing null", ->
clearTimeout(null)
it "can clear the timeout and prevent the cb from being invoked", (done) ->
fn = =>
done(new Error("should not have been invoked"))
timer = setTimeout(fn, 10)
expect(@timer.queue[1].cb).to.eq(fn)
clearTimeout(timer)
expect(@timer.queue).to.deep.eq({})
setTimeout ->
done()
, 20
describe "setInterval", ->
it "returns timer object", (done) ->
obj = setInterval ->
clearInterval(obj)
done()
, 10
expect(obj.id).to.eq(1)
expect(obj.ref).to.be.a("function")
expect(obj.unref).to.be.a("function")
it "increments timer id", (done) ->
fn = _.after 2, ->
clearInterval(obj1)
clearInterval(obj2)
done()
obj1 = setInterval(fn, 10)
obj2 = setInterval(fn, 10000)
expect(obj2.id).to.eq(2)
it "continuously polls until cleared", (done) ->
poller = _.after 3, =>
clearInterval(t)
setTimeout ->
expect(fn).to.be.calledThrice
done()
, 100
fn = @sandbox.spy(poller)
t = setInterval(fn, 10)
describe "clearInterval", ->
it "does not explode when passing null", ->
clearInterval(null)
it "can clear the interval and prevent the cb from being invoked", (done) ->
fn = =>
done(new Error("should not have been invoked"))
timer = setInterval(fn, 10)
expect(@timer.queue[1].cb).to.exist
clearInterval(timer)
expect(@timer.queue).to.deep.eq({})
setTimeout ->
done()
, 20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment