Skip to content

Commit fe2e99c

Browse files
authored
ci: Add internal benchmark tool (#1230)
See: https://github.com/UI5/cli/blob/benchmark/internal/benchmark/README.md JIRA: CPOUI5FOUNDATION-1181
1 parent 7e496de commit fe2e99c

22 files changed

+2321
-0
lines changed

internal/benchmark/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# UI5 CLI Benchmark Tool
2+
3+
A benchmarking utility for measuring and comparing the performance of UI5 CLI commands across different git revisions.
4+
5+
## Prerequisites
6+
7+
This tool requires [hyperfine](https://github.com/sharkdp/hyperfine) to be installed. Follow the installation instructions in the hyperfine repository for your platform.
8+
9+
## Installation
10+
11+
Make the `ui5-cli-benchmark` binary available globally:
12+
13+
```bash
14+
npm link
15+
```
16+
17+
## Usage
18+
19+
```bash
20+
ui5-cli-benchmark run <path-to-config> [<project-dir>...]
21+
```
22+
23+
### Arguments
24+
25+
- `<path-to-config>` - Path to a YAML configuration file (required)
26+
- `[<project-dir>...]` - One or more project directories to benchmark (optional, defaults to current working directory)
27+
28+
### Example
29+
30+
```bash
31+
# Run benchmarks using example config in current directory
32+
ui5-cli-benchmark run config/.example.yaml
33+
34+
# Run benchmarks in specific project directories
35+
ui5-cli-benchmark run config/.example.yaml /path/to/project1 /path/to/project2
36+
```
37+
38+
## Configuration
39+
40+
Create a YAML configuration file with the following structure:
41+
42+
### Revisions
43+
44+
Define the git revisions to benchmark:
45+
46+
```yaml
47+
revisions:
48+
baseline:
49+
name: "Baseline"
50+
revision:
51+
merge_base_from: "feat/example-feature"
52+
target_branch: "main"
53+
example_feature:
54+
name: "Example Feature"
55+
revision: "feat/example-feature"
56+
```
57+
58+
Each revision can specify:
59+
- `name` - Display name for the revision
60+
- `revision` - Either a branch/commit hash or an object with `merge_base_from` and `target_branch` to compute the merge base
61+
62+
### Hyperfine Settings
63+
64+
Configure the benchmark runner (uses [hyperfine](https://github.com/sharkdp/hyperfine)):
65+
66+
```yaml
67+
hyperfine:
68+
warmup: 1 # Number of warmup runs
69+
runs: 10 # Number of benchmark runs
70+
```
71+
72+
### Groups
73+
74+
Define logical groups for organizing benchmark results:
75+
76+
```yaml
77+
groups:
78+
build:
79+
name: "ui5 build"
80+
```
81+
82+
### Benchmarks
83+
84+
Define the commands to benchmark:
85+
86+
```yaml
87+
benchmarks:
88+
- command: "build"
89+
prepare: "rm -rf .ui5-cache" # Optional: command to run before each benchmark
90+
groups:
91+
build:
92+
name: "build"
93+
revisions: # Optional: limit to specific revisions
94+
- "example_feature"
95+
```
96+
97+
Each benchmark can specify:
98+
- `command` - The UI5 CLI command to run (e.g., "build", "build --clean-dest")
99+
- `prepare` - Optional shell command to run before each benchmark iteration
100+
- `groups` - Group(s) this benchmark belongs to with display names
101+
- `revisions` - Optional array to limit which revisions run this benchmark (defaults to all)
102+
103+
## Output
104+
105+
The tool generates:
106+
- Console output with progress and summary
107+
- Markdown report with benchmark results
108+
- JSON report with raw data
109+
110+
Results are organized by revision and group for easy comparison.

internal/benchmark/cli.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env node
2+
3+
import {fileURLToPath} from "node:url";
4+
import path from "node:path";
5+
import fs from "node:fs";
6+
import BenchmarkRunner from "./lib/BenchmarkRunner.js";
7+
import git from "./lib/utils/git.js";
8+
import npm from "./lib/utils/npm.js";
9+
import {spawnProcess} from "./lib/utils/process.js";
10+
11+
const __filename = fileURLToPath(import.meta.url);
12+
const __dirname = path.dirname(__filename);
13+
14+
function printUsageAndExit() {
15+
console.error(
16+
"Usage:\n\t" +
17+
"ui5-cli-benchmark run <path-to-config> [<project-dir>...]"
18+
);
19+
process.exit(1);
20+
}
21+
22+
export const commands = {
23+
async run(args, options = {}) {
24+
const configFilePath = args[0];
25+
const projectDirs = args.slice(1);
26+
27+
// Validate arguments
28+
if (!configFilePath) {
29+
return printUsageAndExit();
30+
}
31+
32+
// Determine repository and CLI paths
33+
const repositoryPath = path.resolve(__dirname, "../..");
34+
const ui5CliPath = path.resolve(repositoryPath, "packages/cli/bin/ui5.cjs");
35+
36+
// Create BenchmarkRunner with injected dependencies
37+
const benchmarkRunner = new BenchmarkRunner({
38+
git: options.git || git,
39+
npm: options.npm || npm,
40+
spawnProcess: options.spawnProcess || spawnProcess,
41+
fs: options.fs || fs
42+
});
43+
44+
// Run benchmarks
45+
const result = await benchmarkRunner.run({
46+
configFilePath,
47+
repositoryPath,
48+
ui5CliPath,
49+
projectDirs: projectDirs.length > 0 ? projectDirs : undefined,
50+
timestamp: options.timestamp
51+
});
52+
53+
if (!result.success) {
54+
process.exit(1);
55+
}
56+
}
57+
};
58+
59+
async function main() {
60+
const args = process.argv.slice(2);
61+
62+
if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
63+
return printUsageAndExit();
64+
}
65+
66+
const command = args[0];
67+
const commandArgs = args.slice(1);
68+
const fn = commands[command];
69+
70+
// Validate command name
71+
if (!fn) {
72+
process.stderr.write(`Unknown command: '${command}'\n\n`);
73+
return process.exit(1);
74+
}
75+
76+
// Execute handler
77+
try {
78+
await fn(commandArgs);
79+
} catch (error) {
80+
console.error(`Unexpected error: ${error.message}`);
81+
console.error("Stack trace:", error.stack);
82+
83+
process.exit(1);
84+
}
85+
}
86+
87+
88+
// Handle uncaught exceptions
89+
process.on("uncaughtException", (error) => {
90+
console.error("Uncaught exception:", error.message);
91+
process.exit(1);
92+
});
93+
94+
process.on("unhandledRejection", (reason, promise) => {
95+
console.error("Unhandled rejection at:", promise, "reason:", reason);
96+
process.exit(1);
97+
});
98+
99+
main();
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Example Benchmark configuration for UI5 CLI
2+
3+
revisions:
4+
baseline:
5+
name: "Baseline"
6+
revision:
7+
merge_base_from: "feat/example-feature"
8+
target_branch: "main"
9+
example_feature:
10+
name: "Example Feature"
11+
revision: "feat/example-feature"
12+
13+
hyperfine:
14+
warmup: 1
15+
runs: 10
16+
17+
groups:
18+
build:
19+
name: "ui5 build"
20+
21+
benchmarks:
22+
23+
- command: "build"
24+
groups:
25+
build:
26+
name: "build"
27+
28+
- command: "build --some-new-flag"
29+
groups:
30+
build:
31+
name: "build (with some new flag)"
32+
revisions:
33+
# Benchmark with some new flag is only relevant for the feature revision,
34+
# as the baseline does not contain the flag
35+
- "example_feature"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import commonConfig from "../../eslint.common.config.js";
2+
3+
export default [
4+
...commonConfig,
5+
{
6+
rules: {
7+
"no-console": "off" // Allow console output in CLI tools
8+
}
9+
}
10+
];

0 commit comments

Comments
 (0)