Skip to content

Commit

Permalink
[FEATURE] minify task: Use workerpool for terser
Browse files Browse the repository at this point in the history
Based on #700

* Changed logging to verbose
* Added option 'useWorkers' to processor. Implicit activation through
  taskUtil parameter seemed a bit odd. Moved that to the minify task
* Added JSDoc
  • Loading branch information
matz3 authored and RandomByte committed Jan 23, 2023
1 parent f9e6f2c commit fd5c77f
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 55 deletions.
128 changes: 75 additions & 53 deletions lib/processors/minifier.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
import path from "node:path";
import {fileURLToPath} from "node:url";
import posixPath from "node:path/posix";
import {minify} from "terser";
import os from "node:os";
import workerpool from "workerpool";
import Resource from "@ui5/fs/Resource";
import logger from "@ui5/logger";
const log = logger.getLogger("builder:processors:minifier");

/**
* Preserve comments which contain:
* <ul>
* <li>copyright notice</li>
* <li>license terms</li>
* <li>"@ui5-bundle"</li>
* <li>"@ui5-bundle-raw-include"</li>
* </ul>
*
* @type {RegExp}
*/
const copyrightCommentsAndBundleCommentPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-Za-z])|released under|license|\u00a9|^@ui5-bundle-raw-include |^@ui5-bundle /i;
const debugFileRegex = /((?:\.view|\.fragment|\.controller|\.designtime|\.support)?\.js)$/;

const MIN_WORKERS = 2;
const MAX_WORKERS = 4;
const osCpus = os.cpus().length || 1;
const maxWorkers = Math.max(Math.min(osCpus - 1, MAX_WORKERS), MIN_WORKERS);

// Shared workerpool across all executions until the taskUtil cleanup is triggered
let pool;

function getPool(taskUtil) {
if (!pool) {
log.verbose(`Creating workerpool with up to ${maxWorkers} workers (available CPU cores: ${osCpus})`);
const workerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "minifierWorker.js");
pool = workerpool.pool(workerPath, {
workerType: "auto",
maxWorkers
});
taskUtil.registerCleanupTask(() => {
log.verbose(`Terminating workerpool`);
const poolToBeTerminated = pool;
pool = null;
poolToBeTerminated.terminate();
});
}
return pool;
}

async function minifyInWorker(options, taskUtil) {
return getPool(taskUtil).exec("execMinification", [options]);
}

/**
* @public
* @module @ui5/builder/processors/minifier
Expand All @@ -40,57 +63,56 @@ const debugFileRegex = /((?:\.view|\.fragment|\.controller|\.designtime|\.suppor
*
* @param {object} parameters Parameters
* @param {@ui5/fs/Resource[]} parameters.resources List of resources to be processed
* @param {@ui5/builder/tasks/TaskUtil|object} [parameters.taskUtil] TaskUtil instance.
* Requred when using the <code>useWorkers</code> option
* @param {object} [parameters.options] Options
* @param {boolean} [parameters.options.addSourceMappingUrl=true]
* Whether to add a sourceMappingURL reference to the end of the minified resource
* Whether to add a sourceMappingURL reference to the end of the minified resource
* @param {boolean} [parameters.options.useWorkers=false]
* Whether to offload the minification task onto separate CPU threads. This often speeds up the build process
* @returns {Promise<module:@ui5/builder/processors/minifier~MinifierResult[]>}
* Promise resolving with object of resource, dbgResource and sourceMap
* Promise resolving with object of resource, dbgResource and sourceMap
*/
export default async function({resources, options: {addSourceMappingUrl = true} = {}}) {
export default async function({resources, taskUtil, options: {addSourceMappingUrl = true, useWorkers = false} = {}}) {
let minify;
if (useWorkers) {
if (!taskUtil) {
// TaskUtil is required for worker support
throw new Error(`Minifier: Option 'useWorkers' requires a taskUtil instance to be provided`);
}
minify = minifyInWorker;
} else {
// Do not use workerpool
minify = (await import("./minifierWorker.js")).default;
}

return Promise.all(resources.map(async (resource) => {
const dbgPath = resource.getPath().replace(debugFileRegex, "-dbg$1");
const dbgResource = await resource.clone();
dbgResource.setPath(dbgPath);

const filename = posixPath.basename(resource.getPath());
const code = await resource.getString();
try {
const sourceMapOptions = {
filename
};
if (addSourceMappingUrl) {
sourceMapOptions.url = filename + ".map";
}
const dbgFilename = posixPath.basename(dbgPath);
const result = await minify({
// Use debug-name since this will be referenced in the source map "sources"
[dbgFilename]: code
}, {
output: {
comments: copyrightCommentsAndBundleCommentPattern,
wrap_func_args: false
},
compress: false,
mangle: {
reserved: [
"jQuery",
"jquery",
"sap",
]
},
sourceMap: sourceMapOptions
});
resource.setString(result.code);
const sourceMapResource = new Resource({
path: resource.getPath() + ".map",
string: result.map
});
return {resource, dbgResource, sourceMapResource};
} catch (err) {
// Note: err.filename contains the debug-name
throw new Error(
`Minification failed with error: ${err.message} in file ${filename} ` +
`(line ${err.line}, col ${err.col}, pos ${err.pos})`);

const sourceMapOptions = {
filename
};
if (addSourceMappingUrl) {
sourceMapOptions.url = filename + ".map";
}
const dbgFilename = posixPath.basename(dbgPath);

const result = await minify({
filename,
dbgFilename,
code,
sourceMapOptions
}, taskUtil);
resource.setString(result.code);
const sourceMapResource = new Resource({
path: resource.getPath() + ".map",
string: result.map
});
return {resource, dbgResource, sourceMapResource};
}));
}
75 changes: 75 additions & 0 deletions lib/processors/minifierWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import workerpool from "workerpool";
import {minify} from "terser";

/**
* @private
* @module @ui5/builder/tasks/minifyWorker
*/

/**
* Preserve comments which contain:
* <ul>
* <li>copyright notice</li>
* <li>license terms</li>
* <li>"@ui5-bundle"</li>
* <li>"@ui5-bundle-raw-include"</li>
* </ul>
*
* @type {RegExp}
*/
const copyrightCommentsAndBundleCommentPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-Za-z])|released under|license|\u00a9|^@ui5-bundle-raw-include |^@ui5-bundle /i;

/**
* Task to minify resources.
*
* @private
* @function default
* @static
*
* @param {object} parameters Parameters
* @param {string} parameters.filename
* @param {string} parameters.dbgFilename
* @param {string} parameters.code
* @param {object} parameters.sourceMapOptions
* @returns {Promise<undefined>} Promise resolving once minification of the resource has finished
*/
export default async function execMinification({
filename,
dbgFilename,
code,
sourceMapOptions
}) {
try {
return await minify({
// Use debug-name since this will be referenced in the source map "sources"
[dbgFilename]: code
}, {
output: {
comments: copyrightCommentsAndBundleCommentPattern,
wrap_func_args: false
},
compress: false,
mangle: {
reserved: [
"jQuery",
"jquery",
"sap",
]
},
sourceMap: sourceMapOptions
});
} catch (err) {
// Note: err.filename contains the debug-name
throw new Error(
`Minification failed with error: ${err.message} in file ${filename} ` +
`(line ${err.line}, col ${err.col}, pos ${err.pos})`);
}
}

if (!workerpool.isMainThread) {
// Script got loaded through workerpool
// => Create a worker and register public functions
workerpool.worker({
execMinification
});
}
4 changes: 3 additions & 1 deletion lib/tasks/minify.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ export default async function({workspace, taskUtil, options: {pattern, omitSourc
const resources = await workspace.byGlob(pattern);
const processedResources = await minifier({
resources,
taskUtil,
options: {
addSourceMappingUrl: !omitSourceMapResources
addSourceMappingUrl: !omitSourceMapResources,
useWorkers: !!taskUtil,
}
});

Expand Down
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"rimraf": "^4.1.1",
"semver": "^7.3.8",
"terser": "^5.16.1",
"workerpool": "^6.3.1",
"xml2js": "^0.4.23"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion test/lib/tasks/minify.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ test("integration: minify omitSourceMapResources=true", async (t) => {
HasDebugVariant: "1️⃣",
IsDebugVariant: "2️⃣",
OmitFromBuildResult: "3️⃣"
}
},
registerCleanupTask: sinon.stub()
};
const {reader, writer, workspace} = createWorkspace();
const content = `
Expand Down

0 comments on commit fd5c77f

Please sign in to comment.