Search code examples
javascriptnode.jselectronnightmare

Nightmare Js Evaluation timed out after multiple evaluations


I'm trying to setup an app I've been developing that involves recording an HTML widget (with animations) and turning it into an mp4. Locally it works great. I have Nightmare Js handling the screenshots and FFMPEG converting the screenshots into an mp4, which takes around 90s on my PC.

I'm setting it up in a docker container through a hosting service, and cannot get past this issue. My test script at the moment looks something like this:

const Nightmare = require('nightmare')
const Xvfb = require('xvfb')

module.exports = function(app) {
  app.get('/api/nightmare', async (req, res) => {
    console.log('GET request to /api/nightmare')

    try {
      // Start xvfb and create our nightmare object
      const close = await xvfb()
      const nightmare = Nightmare({ executionTimeout: 1000 })

      const [err, result] = await poss(run(nightmare))
      if (err) {
        // Cleanup before throwing error
        await nightmare.end()
        await close()
        throw err
      }

      // shut'er down
      await nightmare.end()
      await close()
      res.status(200).send('Completed')
    } catch (error) {
      console.log(error)
      res.status(500).send(error)
    }
  })  
}

async function run(nightmare) {
  var data = {
    'id': '2OWBTMUL',
    'somedata': 'blah'
    // More data pulled from the database goes here
  }

  // Create folder to hold images
  var procName = new Date().getTime() + '_testing_' + data.id
  fs.mkdirSync(path.resolve(__dirname, '../processing/', procName))

  // URL to inject into webpage so the webpage can access the data
  var url = `http://example.com?` +
    `id=${data.id}&` +
    `somedata=${data.somedata}`

  console.log('Starting NightmareJs')
  await nightmare
    .viewport(1920, 1080)
    .goto('file:///' + path.resolve(__dirname, '../templates/example.html'))
    .evaluate(newUrl => {
      // Set url and get the page ready for recording
      url = new URL(newUrl)
      initiate()
      start()
      timeline.pause()
    }, url)
    .catch(error => { console.log(error) })

  // Take 200 screenshots (8s @ 25fps)
  var frames = 200
  for (var i = 0; i < frames; i++) {
    console.log('Taking screenshot ' + i)

    await nightmare
      .screenshot(path.resolve(__dirname, '../processing/', procName, 'screen_' + i + '.png'))
      .evaluate(shot => { timeline.seek((8 / 200) * shot) }, i)
      .catch(error => { console.log(error) })
  }

  console.log('Done.')
}

// xvfb wrapper
function xvfb(options) {
  var xvfb = new Xvfb(options)

  function close() {
    return new Promise((resolve, reject) => {
      xvfb.stop(err => (err ? reject(err) : resolve()))
    })
  }

  return new Promise((resolve, reject) => {
    xvfb.start(err => (err ? reject(err) : resolve(close)))
  })
}

// try/catch helper
async function poss(promise) {
  try {
    const result = await promise
    return [null, result]
  } catch (err) {
    return [err, null]
  }
}

Some environment details:
Ubuntu 18.04.1
Supervisor
Node 8.12.0 (DEBUG=* set)

The issue: After about 17 loops, Nightmare throws two different errors. In my nodejs-stderr.log file, I get this

Taking screenshot x
Error: Evaluation timed out after 1000msec.  Are you calling done() or resolving your promises?

Testing with the done() callback or promise in the evaluation didn't change anything, so I kept it simple

In my nodejs-stdout.log file, I get this for every successful loop

Mon, 19 Nov 2018 09:37:06 GMT nightmare:actions .screenshot()
Mon, 19 Nov 2018 09:37:06 GMT nightmare:log subscribing to browser window frames
Mon, 19 Nov 2018 09:37:06 GMT nightmare:log Highlighting page to trigger rendering.
Mon, 19 Nov 2018 09:37:06 GMT nightmare:log unsubscribing from browser window frames
Mon, 19 Nov 2018 09:37:07 GMT nightmare:actions .screenshot() captured with length 1092963
Mon, 19 Nov 2018 09:37:07 GMT nightmare:actions .evaluate() fn on the page
Mon, 19 Nov 2018 09:37:07 GMT nightmare queueing action "screenshot"
Mon, 19 Nov 2018 09:37:07 GMT nightmare queueing action "evaluate"
Mon, 19 Nov 2018 09:37:07 GMT nightmare running

And then after 17 or so loops, I get:

Mon, 19 Nov 2018 09:37:07 GMT nightmare:actions .screenshot()
Mon, 19 Nov 2018 09:37:07 GMT nightmare:log subscribing to browser window frames
Mon, 19 Nov 2018 09:37:07 GMT nightmare:log Highlighting page to trigger rendering.
Tue, 20 Nov 2018 02:11:43 GMT nightmare:log crashed [{},false]
Mon, 19 Nov 2018 09:37:12 GMT nightmare:log FrameManager timing out after 1000 ms with no new rendered frames
Mon, 19 Nov 2018 09:37:12 GMT nightmare:log unsubscribing from browser window frames
Mon, 19 Nov 2018 09:37:12 GMT nightmare:actions .screenshot() captured with length 0
Mon, 19 Nov 2018 09:37:12 GMT nightmare:actions .evaluate() fn on the page
Mon, 19 Nov 2018 09:37:13 GMT nightmare queueing action "screenshot"
Mon, 19 Nov 2018 09:37:13 GMT nightmare queueing action "evaluate"
Mon, 19 Nov 2018 09:37:13 GMT nightmare running

And the rest all fail, without the crashed [{},false] message.
Removing .evaluate(shot => { timeline.seek((8 / 200) * shot) }, i) in the loop solves the issue, but I need that otherwise it's just a motionless video! I have tried separate calls (not chaining evaluate and screenshot), putting .wait(200) between stuff, but I can't figure it out or find any help on this issue.


Solution

  • Turned out it was the shared memory being too small. I assume when the images were all loaded it exceeded the memory limit and Electron crashed. After upping the shared memory from 64MB to 128MB, the issue is solved.