Skip to content

Commit

Permalink
feat: migrate to puppeteer-cluster
Browse files Browse the repository at this point in the history
Instead of managing chrome instances myself, we've migrated to
puppeteer-cluster to handle all that for us.
  • Loading branch information
jniles committed May 28, 2020
1 parent 8c3ec26 commit 0cc65b0
Show file tree
Hide file tree
Showing 4 changed files with 366 additions and 371 deletions.
125 changes: 47 additions & 78 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,53 @@
const pptr = require('puppeteer');
const { Cluster } = require('puppeteer-cluster');
const { inlineSource } = require('inline-source');

const pptrOptions = {
headless : true,
args : [
'--bwsi',
'--disable-default-apps',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-sandbox',
'--hide-scrollbars',
'--disable-web-security',
],
};
const debug = require('debug')('imaworldhealth:coral');

const DEFAULTS = {
preferCSSPageSize : true,
format : 'A4',
swallowErrors : true,
};

let browser;
// launch cluster
let cluster;

/**
* PDF rendering is extremely resource-intensive if we do not reuse browser instances
* On a RPi V4, we have a 17 second startup by launching a new browser each time. By
* reusing the same chromium instance, we shave that to sub-second timing.
*/
const launch = async () => {
debug('setting up puppeteer cluster');
cluster = await Cluster.launch({
concurrency : Cluster.CONCURRENCY_CONTEXT, // incognito windows
maxConcurrency : 2,
});

debug('configuring PDF rendering task');

await cluster.task(async ({ page, data }) => {
if (data.options.filename) {
debug(`rendering PDF w/ filename: ${data.options.filename}`);
}

/**
* @function launchNewBrowser()
*
* @description
* Replaces the global "browser" variable with a fresh chromium instance.
*
*/
function launchNewBrowser() {
browser = pptr.launch(pptrOptions);
}
await page.setContent(data.html.trim());

const hasBrowserReuseFlag = process.env.CORAL_REUSE_BROWSER;
if (hasBrowserReuseFlag) {
launchNewBrowser();
}
// FIXME(@jniles) - for some reason, puppeteer seems to be inconsistent on the
// kind of page rendering sizes, but this seems to work for making pages landscaped.
// See: https://github.com/puppeteer/puppeteer/issues/3834#issuecomment-549007667
if (data.options.orientation === 'landscape') {
await page.addStyleTag(
{ content : '@page { size: A4 landscape; }' },
);
}

/**
* @function getBrowserInstance
*
* @description
* Allows us to reuse browser instances as needed.
*/
function getBrowserInstance() {
return hasBrowserReuseFlag
? browser
: pptr.launch(pptrOptions);
}
return page.pdf(data.options);
});

debug('rendering task configured');

return cluster;
};

/**
* @function render
Expand All @@ -69,50 +62,26 @@ function getBrowserInstance() {
* @returns {Promise} a PDF of the HTML source
*/
async function render(html, options = {}) {
try {
const opts = { ...options, ...DEFAULTS };

let inlined = html;
if (!options.skipRendering) {
inlined = await inlineSource(html, {
attribute : false, rootpath : '/', compress : false, swallowErrors : opts.swallowErrors,
});
}

browser = await getBrowserInstance();
const page = await browser.newPage();
await page.setContent(inlined.trim());
const opts = { ...options, ...DEFAULTS };

// FIXME(@jniles) - for some reason, puppeteer seems to be inconsistent on the
// kind of page rendering sizes, but this seems to work for making pages landscaped.
// See: https://github.com/puppeteer/puppeteer/issues/3834#issuecomment-549007667
if (opts.orientation === 'landscape') {
await page.addStyleTag(
{ content : '@page { size: A4 landscape; }' },
);
}

const pdf = await page.pdf(opts);

// clean up listeners
page.removeAllListeners();
let inlined = html;
if (!options.skipRendering) {
inlined = await inlineSource(html, {
attribute : false, rootpath : '/', compress : false, swallowErrors : opts.swallowErrors,
});
}

await page.close();
return pdf;
} catch (e) {
if (browser) {
browser.removeAllListeners();
await browser.close();
}
if (!cluster) { cluster = await launch(); }

launchNewBrowser();
return null;
}
// make sure cluster is setup
const pdf = await cluster.execute({ options : opts, html : inlined });
return pdf;
}

// make sure cluster is terminated correctly on exit
process.on('exit', async () => {
browser.removeAllListeners();
await browser.close();
await cluster.idle();
await cluster.close();
});

module.exports = render;
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
}
},
"dependencies": {
"debug": "^4.1.1",
"inline-source": "7.2.0",
"puppeteer": "3.1.0"
"puppeteer": "3.1.0",
"puppeteer-cluster": "^0.21.0"
},
"devDependencies": {
"ava": "3.8.2",
"eslint": "6.8.0",
"eslint": "7.1.0",
"eslint-config-airbnb-base": "14.1.0",
"eslint-plugin-import": "2.20.2",
"file-type": "14.5.0",
Expand Down
4 changes: 4 additions & 0 deletions test/pdf.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ test('renders a PDF buffer from an html file', async (t) => {
const type = await fileType.fromBuffer(rendered);
t.is(type.mime, 'application/pdf');
});

test('throws an error if no parameters are provided', async (t) => {
await t.throwsAsync(() => render());
});
Loading

0 comments on commit 0cc65b0

Please sign in to comment.