Skip to content

Commit

Permalink
[INTERNAL] Add CPU profile generation capability
Browse files Browse the repository at this point in the history
Can be activated by setting the `UI5LINT_PROFILE` environment variable
to a truthy value.

Based on SAP/ui5-cli#638
  • Loading branch information
RandomByte committed Mar 7, 2024
1 parent d15313d commit e1802ed
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 1 deletion.
11 changes: 10 additions & 1 deletion packages/ui5-linter/src/cli/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ async function handleLint(argv: yargs.ArgumentsCamelCase) {
format
} = <{ coverage: boolean, filePaths: string[], details: boolean, format: string }><unknown>argv;

let profile;
if (process.env.UI5LINT_PROFILE) {
profile = await import("./utils/profile.js");
await profile.start();
}

const res = await lintProject({
rootDir: path.join(process.cwd()),
filePaths: filePaths && filePaths.map((filePath) => path.resolve(process.cwd(), filePath)),
Expand All @@ -113,7 +119,10 @@ async function handleLint(argv: yargs.ArgumentsCamelCase) {
const textFormatter = new Text();
process.stderr.write(textFormatter.format(res, details));
}

// Stop profiling after CLI finished execution
if (profile) {
await profile.stop();
}
}

export default function base(cli: yargs.Argv) {
Expand Down
98 changes: 98 additions & 0 deletions packages/ui5-linter/src/cli/utils/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {writeFile} from "node:fs/promises";
import {Session, Profiler} from "node:inspector";
import {getLogger} from "@ui5/logger";
const log = getLogger("cli:utils:profile");

let session: Session | null;
type ProcessSignals = Record<string, (exitCode: number) => void>
let processSignals: ProcessSignals | null;

export async function start() {
if (session) {
return;
}
session = new Session();
session.connect();
await new Promise<void>((resolve) => {
session?.post("Profiler.enable", () => {
log.info(`Recording CPU profile...`);
session?.post("Profiler.start", () => {
processSignals = registerSigHooks();
resolve();
});
});
});
}

async function writeProfile(profile: Profiler.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`;
log.info(`\nSaving CPU profile to ${fileName}...`);
await writeFile(fileName, JSON.stringify(profile));
}

export async function stop() {
if (!session) {
return;
}
if (processSignals) {
deregisterSigHooks(processSignals);
processSignals = null;
}
const profile = await new Promise<Profiler.Profile | null>((resolve) => {
session?.post("Profiler.stop", (err, {profile}) => {
if (err) {
resolve(null);
} else {
resolve(profile);
}
});
session = null;
});
if (profile) {
await writeProfile(profile);
}
}

function registerSigHooks() {
function createListener(exitCode: number) {
return function() {
// Gracefully end profiling, then exit
stop().then(() => {
process.exit(exitCode);
});
};
}

const processSignals: 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: ProcessSignals) {
for (const signal of Object.keys(signals)) {
process.removeListener(signal, signals[signal]);
}
}

0 comments on commit e1802ed

Please sign in to comment.