Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add --profile option to lerna exec and lerna run #2376

Merged
merged 16 commits into from Dec 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 24 additions & 0 deletions commands/exec/README.md
Expand Up @@ -79,3 +79,27 @@ Pass `--no-bail` to disable this behavior, executing in _all_ packages regardles

Disable package name prefixing when output is streaming (`--stream` _or_ `--parallel`).
This option can be useful when piping results to other processes, such as editor plugins.

### `--profile`

Profiles the command executions and produces a performance profile which can be analyzed using DevTools in a
Chromium-based browser (direct url: `devtools://devtools/bundled/devtools_app.html`). The profile shows a timeline of
the command executions where each execution is assigned to an open slot. The number of slots is determined by the
`--concurrency` option and the number of open slots is determined by `--concurrency` minus the number of ongoing
operations. The end result is a visualization of the parallel execution of your commands.

The default location of the performance profile output is at the root of your project.

```sh
$ lerna exec --profile -- <command>
```

> **Note:** Lerna will only profile when topological sorting is enabled (i.e. without `--parallel` and `--no-sort`).

### `--profile-location <location>`

You can provide a custom location for the performance profile output. The path provided will be resolved relative to the current working directory.

```sh
$ lerna exec --profile --profile-location=logs/profile/ -- <command>
```
38 changes: 38 additions & 0 deletions commands/exec/__tests__/exec-command.test.js
@@ -1,6 +1,8 @@
"use strict";

const path = require("path");
const fs = require("fs-extra");
const globby = require("globby");

// mocked modules
const ChildProcessUtilities = require("@lerna/child-process");
Expand Down Expand Up @@ -191,6 +193,42 @@ describe("ExecCommand", () => {
});
});

describe("with --profile", () => {
it("executes a profiled command in all packages", async () => {
const cwd = await initFixture("basic");

await lernaExec(cwd)("--profile", "--", "ls");

const [profileLocation] = await globby("Lerna-Profile-*.json", { cwd, absolute: true });
const json = await fs.readJson(profileLocation);

expect(json).toMatchObject([
{
name: "package-1",
ph: "X",
ts: expect.any(Number),
pid: 1,
tid: expect.any(Number),
dur: expect.any(Number),
},
{
name: "package-2",
},
]);
});

it("accepts --profile-location", async () => {
const cwd = await initFixture("basic");

await lernaExec(cwd)("--profile", "--profile-location", "foo/bar", "--", "ls");

const [profileLocation] = await globby("foo/bar/Lerna-Profile-*.json", { cwd, absolute: true });
const exists = await fs.exists(profileLocation);

expect(exists).toBe(true);
});
});

describe("with --no-sort", () => {
it("runs commands in lexical (not topological) order", async () => {
const testDir = await initFixture("toposort");
Expand Down
10 changes: 10 additions & 0 deletions commands/exec/command.js
Expand Up @@ -57,6 +57,16 @@ exports.builder = yargs => {
hidden: true,
type: "boolean",
},
profile: {
group: "Command Options:",
describe: "Profile command executions and output performance profile to default location.",
type: "boolean",
},
"profile-location": {
group: "Command Options:",
describe: "Output performance profile to custom location instead of default project root.",
type: "string",
},
});

return filterable(yargs);
Expand Down
37 changes: 29 additions & 8 deletions commands/exec/index.js
Expand Up @@ -4,6 +4,7 @@ const pMap = require("p-map");

const ChildProcessUtilities = require("@lerna/child-process");
const Command = require("@lerna/command");
const Profiler = require("@lerna/profiler");
const runTopologically = require("@lerna/run-topologically");
const ValidationError = require("@lerna/validation-error");
const { getFilteredPackages } = require("@lerna/filter-options");
Expand Down Expand Up @@ -119,27 +120,47 @@ class ExecCommand extends Command {
};
}

runCommandInPackagesTopological() {
const runner = this.options.stream
getRunner() {
return this.options.stream
? pkg => this.runCommandInPackageStreaming(pkg)
: pkg => this.runCommandInPackageCapturing(pkg);
}

runCommandInPackagesTopological() {
let profiler;
let runner;

if (this.options.profile) {
profiler = new Profiler({
concurrency: this.concurrency,
log: this.logger,
outputDirectory: this.options.profileLocation || this.project.rootPath,
});

const callback = this.getRunner();
runner = pkg => profiler.run(() => callback(pkg), pkg.name);
} else {
runner = this.getRunner();
}

return runTopologically(this.filteredPackages, runner, {
let chain = runTopologically(this.filteredPackages, runner, {
concurrency: this.concurrency,
rejectCycles: this.options.rejectCycles,
});

if (profiler) {
chain = chain.then(results => profiler.output().then(() => results));
}

return chain;
}

runCommandInPackagesParallel() {
return pMap(this.filteredPackages, pkg => this.runCommandInPackageStreaming(pkg));
}

runCommandInPackagesLexical() {
const runner = this.options.stream
? pkg => this.runCommandInPackageStreaming(pkg)
: pkg => this.runCommandInPackageCapturing(pkg);

return pMap(this.filteredPackages, runner, { concurrency: this.concurrency });
return pMap(this.filteredPackages, this.getRunner(), { concurrency: this.concurrency });
}

runCommandInPackageStreaming(pkg) {
Expand Down
1 change: 1 addition & 0 deletions commands/exec/package.json
Expand Up @@ -35,6 +35,7 @@
"@lerna/child-process": "file:../../core/child-process",
"@lerna/command": "file:../../core/command",
"@lerna/filter-options": "file:../../core/filter-options",
"@lerna/profiler": "file:../../utils/profiler",
"@lerna/run-topologically": "file:../../utils/run-topologically",
"@lerna/validation-error": "file:../../core/validation-error",
"p-map": "^2.1.0"
Expand Down
24 changes: 24 additions & 0 deletions commands/run/README.md
Expand Up @@ -82,3 +82,27 @@ Pass `--no-bail` to disable this behavior, running the script in _all_ packages

Disable package name prefixing when output is streaming (`--stream` _or_ `--parallel`).
This option can be useful when piping results to other processes, such as editor plugins.

### `--profile`

Profiles the script executions and produces a performance profile which can be analyzed using DevTools in a
Chromium-based browser (direct url: `devtools://devtools/bundled/devtools_app.html`). The profile shows a timeline of
the script executions where each execution is assigned to an open slot. The number of slots is determined by the
`--concurrency` option and the number of open slots is determined by `--concurrency` minus the number of ongoing
operations. The end result is a visualization of the parallel execution of your scripts.

The default location of the performance profile output is at the root of your project.

```sh
$ lerna run build --profile
```

> **Note:** Lerna will only profile when topological sorting is enabled (i.e. without `--parallel` and `--no-sort`).

### `--profile-location <location>`

You can provide a custom location for the performance profile output. The path provided will be resolved relative to the current working directory.

```sh
$ lerna run build --profile --profile-location=logs/profile/
```
39 changes: 39 additions & 0 deletions commands/run/__tests__/run-command.test.js
Expand Up @@ -2,6 +2,9 @@

jest.mock("@lerna/npm-run-script");

const fs = require("fs-extra");
const globby = require("globby");

// mocked modules
const npmRunScript = require("@lerna/npm-run-script");
const output = require("@lerna/output");
Expand Down Expand Up @@ -161,6 +164,42 @@ describe("RunCommand", () => {
});
});

describe("with --profile", () => {
it("executes a profiled command in all packages", async () => {
const cwd = await initFixture("basic");

await lernaRun(cwd)("--profile", "my-script");

const [profileLocation] = await globby("Lerna-Profile-*.json", { cwd, absolute: true });
const json = await fs.readJson(profileLocation);

expect(json).toMatchObject([
{
name: "package-1",
ph: "X",
ts: expect.any(Number),
pid: 1,
tid: expect.any(Number),
dur: expect.any(Number),
},
{
name: "package-3",
},
]);
});

it("accepts --profile-location", async () => {
const cwd = await initFixture("basic");

await lernaRun(cwd)("--profile", "--profile-location", "foo/bar", "my-script");

const [profileLocation] = await globby("foo/bar/Lerna-Profile-*.json", { cwd, absolute: true });
const exists = await fs.exists(profileLocation);

expect(exists).toBe(true);
});
});

describe("with --no-sort", () => {
it("runs scripts in lexical (not topological) order", async () => {
const testDir = await initFixture("toposort");
Expand Down
10 changes: 10 additions & 0 deletions commands/run/command.js
Expand Up @@ -59,6 +59,16 @@ exports.builder = yargs => {
hidden: true,
type: "boolean",
},
profile: {
group: "Command Options:",
describe: "Profile script executions and output performance profile to default location.",
type: "boolean",
},
"profile-location": {
group: "Command Options:",
describe: "Output performance profile to custom location instead of default project root.",
type: "string",
},
});

return filterable(yargs);
Expand Down
37 changes: 29 additions & 8 deletions commands/run/index.js
Expand Up @@ -5,6 +5,7 @@ const pMap = require("p-map");
const Command = require("@lerna/command");
const npmRunScript = require("@lerna/npm-run-script");
const output = require("@lerna/output");
const Profiler = require("@lerna/profiler");
const timer = require("@lerna/timer");
const runTopologically = require("@lerna/run-topologically");
const ValidationError = require("@lerna/validation-error");
Expand Down Expand Up @@ -127,27 +128,47 @@ class RunCommand extends Command {
};
}

runScriptInPackagesTopological() {
const runner = this.options.stream
getRunner() {
return this.options.stream
? pkg => this.runScriptInPackageStreaming(pkg)
: pkg => this.runScriptInPackageCapturing(pkg);
}

runScriptInPackagesTopological() {
let profiler;
let runner;

if (this.options.profile) {
profiler = new Profiler({
concurrency: this.concurrency,
log: this.logger,
outputDirectory: this.options.profileLocation,
});

const callback = this.getRunner();
runner = pkg => profiler.run(() => callback(pkg), pkg.name);
} else {
runner = this.getRunner();
}

return runTopologically(this.packagesWithScript, runner, {
let chain = runTopologically(this.packagesWithScript, runner, {
concurrency: this.concurrency,
rejectCycles: this.options.rejectCycles,
});

if (profiler) {
chain = chain.then(results => profiler.output().then(() => results));
}

return chain;
}

runScriptInPackagesParallel() {
return pMap(this.packagesWithScript, pkg => this.runScriptInPackageStreaming(pkg));
}

runScriptInPackagesLexical() {
const runner = this.options.stream
? pkg => this.runScriptInPackageStreaming(pkg)
: pkg => this.runScriptInPackageCapturing(pkg);

return pMap(this.packagesWithScript, runner, { concurrency: this.concurrency });
return pMap(this.packagesWithScript, this.getRunner(), { concurrency: this.concurrency });
}

runScriptInPackageStreaming(pkg) {
Expand Down
1 change: 1 addition & 0 deletions commands/run/package.json
Expand Up @@ -36,6 +36,7 @@
"@lerna/filter-options": "file:../../core/filter-options",
"@lerna/npm-run-script": "file:../../utils/npm-run-script",
"@lerna/output": "file:../../utils/output",
"@lerna/profiler": "file:../../utils/profiler",
"@lerna/run-topologically": "file:../../utils/run-topologically",
"@lerna/timer": "file:../../utils/timer",
"@lerna/validation-error": "file:../../core/validation-error",
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

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

9 changes: 9 additions & 0 deletions utils/profiler/README.md
@@ -0,0 +1,9 @@
# `@lerna/profiler`

> An internal Lerna tool

## Usage

You probably shouldn't, at least directly.

Install [lerna](https://www.npmjs.com/package/lerna) for access to the `lerna` CLI.