Skip to content

Commit 5ca3112

Browse files
committed
feat(cli): Add CPU profiling support
Profiling can be enabled via the `UI5_CLI_PROFILE` environment variable.
1 parent 8e97148 commit 5ca3112

3 files changed

Lines changed: 98 additions & 4 deletions

File tree

packages/cli/bin/ui5.cjs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,19 @@ const ui5 = {
9494
},
9595

9696
async invokeCLI(pkg) {
97+
let profile;
98+
if (process.env.UI5_CLI_PROFILE) {
99+
profile = await import("../lib/utils/profile.js");
100+
await profile.start();
101+
}
97102
const {default: cli} = await import("../lib/cli/cli.js");
98-
await cli(pkg);
103+
const {command} = await cli(pkg);
104+
105+
// Stop profiling after CLI finished execution
106+
// Except for "serve" command, which continues running and only stops on sigint (see profile.js)
107+
if (profile && command !== "serve") {
108+
await profile.stop();
109+
}
99110
},
100111

101112
async main() {

packages/cli/lib/cli/cli.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ export default async (pkg) => {
6868
// Format terminal output to full available width
6969
cli.wrap(cli.terminalWidth());
7070

71-
// yargs registers a get method on the argv property.
72-
// The property needs to be accessed to initialize everything.
73-
cli.argv;
71+
const {_} = await cli.argv;
72+
return {
73+
command: _[0]
74+
};
7475
};

packages/cli/lib/utils/profile.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import {writeFileSync} from "node:fs";
2+
import {Session} from "node:inspector/promises";
3+
4+
let session;
5+
let processSignals;
6+
7+
export async function start() {
8+
if (session) {
9+
return;
10+
}
11+
session = new Session();
12+
session.connect();
13+
await session.post("Profiler.enable");
14+
await session.post("Profiler.start");
15+
console.log(`Recording CPU profile...`);
16+
processSignals = registerSigHooks();
17+
}
18+
19+
async function writeProfile(profile) {
20+
const formatter = new Intl.DateTimeFormat("en-GB", {
21+
year: "numeric",
22+
month: "2-digit",
23+
day: "2-digit",
24+
hour: "2-digit",
25+
minute: "2-digit",
26+
second: "2-digit",
27+
});
28+
const dateParts = Object.create(null);
29+
const parts = formatter.formatToParts(new Date());
30+
parts.forEach((p) => {
31+
dateParts[p.type] = p.value;
32+
});
33+
34+
const fileName = `./ui5_${dateParts.year}-${dateParts.month}-${dateParts.day}_` +
35+
`${dateParts.hour}-${dateParts.minute}-${dateParts.second}.cpuprofile`;
36+
console.log(`\nSaving CPU profile to ${fileName}...`);
37+
writeFileSync(fileName, JSON.stringify(profile));
38+
}
39+
40+
export async function stop() {
41+
if (!session) {
42+
return;
43+
}
44+
const {profile} = await session.post("Profiler.stop");
45+
session = null;
46+
if (profile) {
47+
await writeProfile(profile);
48+
}
49+
if (processSignals) {
50+
deregisterSigHooks(processSignals);
51+
processSignals = null;
52+
}
53+
}
54+
55+
function registerSigHooks() {
56+
function createListener(exitCode) {
57+
return function() {
58+
// Gracefully end profiling, then exit
59+
stop().then(() => {
60+
process.exit(exitCode);
61+
});
62+
};
63+
}
64+
65+
const processSignals = {
66+
"SIGHUP": createListener(128 + 1),
67+
"SIGINT": createListener(128 + 2),
68+
"SIGTERM": createListener(128 + 15),
69+
"SIGBREAK": createListener(128 + 21)
70+
};
71+
72+
for (const signal of Object.keys(processSignals)) {
73+
process.on(signal, processSignals[signal]);
74+
}
75+
return processSignals;
76+
}
77+
78+
function deregisterSigHooks(signals) {
79+
for (const signal of Object.keys(signals)) {
80+
process.removeListener(signal, signals[signal]);
81+
}
82+
}

0 commit comments

Comments
 (0)