Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: Add --profile option to lerna exec and lerna run (#2376)
* Add `--profile` option to run
* Add `--profile` option to exec
* Add `@lerna/profiler` package
  • Loading branch information
bweggersen authored and evocateur committed Dec 27, 2019
1 parent ec0f92a commit 6290174
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 16 deletions.
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.

0 comments on commit 6290174

Please sign in to comment.