diff --git a/packages/cli/bin/ui5.cjs b/packages/cli/bin/ui5.cjs index 9ded92bba5a..187e609d603 100755 --- a/packages/cli/bin/ui5.cjs +++ b/packages/cli/bin/ui5.cjs @@ -94,8 +94,19 @@ const ui5 = { }, async invokeCLI(pkg) { + let profile; + if (process.env.UI5_CLI_PROFILE) { + profile = await import("../lib/utils/profile.js"); + await profile.start(); + } const {default: cli} = await import("../lib/cli/cli.js"); - await cli(pkg); + const argv = await cli(pkg); + + // Stop profiling after CLI finished execution + // Except for "serve" command, which continues running and only stops on sigint (see profile.js) + if (profile && argv._[0] !== "serve") { + await profile.stop(); + } }, async main() { diff --git a/packages/cli/lib/cli/cli.js b/packages/cli/lib/cli/cli.js index e46a519368c..7366bc321ad 100644 --- a/packages/cli/lib/cli/cli.js +++ b/packages/cli/lib/cli/cli.js @@ -68,7 +68,5 @@ export default async (pkg) => { // Format terminal output to full available width cli.wrap(cli.terminalWidth()); - // yargs registers a get method on the argv property. - // The property needs to be accessed to initialize everything. - cli.argv; + return cli.parse(); }; diff --git a/packages/cli/lib/utils/profile.js b/packages/cli/lib/utils/profile.js new file mode 100644 index 00000000000..295bc8789e3 --- /dev/null +++ b/packages/cli/lib/utils/profile.js @@ -0,0 +1,83 @@ +/* eslint-disable no-console */ +import {writeFileSync} from "node:fs"; +import {Session} from "node:inspector/promises"; + +let session; +let processSignals; + +export async function start() { + if (session) { + return; + } + session = new Session(); + session.connect(); + await session.post("Profiler.enable"); + await session.post("Profiler.start"); + console.log(`Recording CPU profile...`); + processSignals = registerSigHooks(); +} + +async function writeProfile(profile) { + const formatter = new Intl.DateTimeFormat("en-GB", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + const dateParts = Object.create(null); + const parts = formatter.formatToParts(new Date()); + parts.forEach((p) => { + dateParts[p.type] = p.value; + }); + + const fileName = `./ui5_${dateParts.year}-${dateParts.month}-${dateParts.day}_` + + `${dateParts.hour}-${dateParts.minute}-${dateParts.second}.cpuprofile`; + console.log(`\nSaving CPU profile to ${fileName}...`); + writeFileSync(fileName, JSON.stringify(profile)); +} + +export async function stop() { + if (!session) { + return; + } + const {profile} = await session.post("Profiler.stop"); + session = null; + if (profile) { + await writeProfile(profile); + } + if (processSignals) { + deregisterSigHooks(processSignals); + processSignals = null; + } +} + +function registerSigHooks() { + function createListener(exitCode) { + return function() { + // Gracefully end profiling, then exit + stop().then(() => { + process.exit(exitCode); + }); + }; + } + + const processSignals = { + "SIGHUP": createListener(128 + 1), + "SIGINT": createListener(128 + 2), + "SIGTERM": createListener(128 + 15), + "SIGBREAK": createListener(128 + 21) + }; + + for (const signal of Object.keys(processSignals)) { + process.on(signal, processSignals[signal]); + } + return processSignals; +} + +function deregisterSigHooks(signals) { + for (const signal of Object.keys(signals)) { + process.removeListener(signal, signals[signal]); + } +} diff --git a/packages/cli/test/lib/cli/cli.js b/packages/cli/test/lib/cli/cli.js index c6718bc0ed2..1854656a237 100644 --- a/packages/cli/test/lib/cli/cli.js +++ b/packages/cli/test/lib/cli/cli.js @@ -11,7 +11,8 @@ test.beforeEach(async (t) => { notify: t.context.updateNotifierNotify }).named("updateNotifier"); - t.context.argvGetter = sinon.stub(); + t.context.yargsHideBin = sinon.stub().named("hideBin").returns([]); + t.context.yargsInstance = { parserConfiguration: sinon.stub(), version: sinon.stub(), @@ -19,10 +20,7 @@ test.beforeEach(async (t) => { command: sinon.stub(), terminalWidth: sinon.stub().returns(123), wrap: sinon.stub(), - get argv() { - t.context.argvGetter(); - return undefined; - } + parse: sinon.stub().resolves({_: []}) }; t.context.yargs = sinon.stub().returns(t.context.yargsInstance).named("yargs"); @@ -54,10 +52,9 @@ test.beforeEach(async (t) => { t.context.cli = await esmock.p("../../../lib/cli/cli.js", { "update-notifier": t.context.updateNotifier, "yargs": t.context.yargs, - // TODO: Somehow esmock is unable to mock this import - // "yargs/helpers": { - // hideBin: t.context.yargsHideBin - // }, + "yargs/helpers": { + hideBin: t.context.yargsHideBin + }, "../../../lib/cli/version.js": { setVersion: t.context.setVersion }, @@ -76,7 +73,7 @@ test.afterEach.always((t) => { test.serial("CLI", async (t) => { const { - cli, updateNotifier, updateNotifierNotify, argvGetter, yargsInstance, yargs, + cli, updateNotifier, updateNotifierNotify, yargsInstance, yargs, setVersion, cliBase } = t.context; @@ -131,8 +128,8 @@ test.serial("CLI", async (t) => { t.is(yargsInstance.wrap.callCount, 1); t.deepEqual(yargsInstance.wrap.getCall(0).args, [123]); - t.is(argvGetter.callCount, 1); - t.deepEqual(argvGetter.getCall(0).args, []); + t.is(yargsInstance.parse.callCount, 1); + t.deepEqual(yargsInstance.parse.getCall(0).args, []); sinon.assert.callOrder( updateNotifier, @@ -146,7 +143,7 @@ test.serial("CLI", async (t) => { yargsInstance.command, yargsInstance.terminalWidth, yargsInstance.wrap, - argvGetter + yargsInstance.parse ); });