diff --git a/bin/ui5.cjs b/bin/ui5.cjs index 2b84f795..0d380bfe 100755 --- a/bin/ui5.cjs +++ b/bin/ui5.cjs @@ -90,8 +90,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 {command} = 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 && command !== "serve") { + await profile.stop(); + } }, async main() { diff --git a/lib/cli/base.js b/lib/cli/base.js index 6cd206b2..a6d271ce 100644 --- a/lib/cli/base.js +++ b/lib/cli/base.js @@ -138,4 +138,3 @@ export default function(cli) { process.exit(1); }); } - diff --git a/lib/cli/cli.js b/lib/cli/cli.js index e46a5193..b37c2fc3 100644 --- a/lib/cli/cli.js +++ b/lib/cli/cli.js @@ -68,7 +68,8 @@ 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; + const {_} = await cli.argv; + return { + command: _[0] + }; }; diff --git a/lib/utils/profile.js b/lib/utils/profile.js new file mode 100644 index 00000000..2e786646 --- /dev/null +++ b/lib/utils/profile.js @@ -0,0 +1,82 @@ +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]); + } +}