Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 84f5278

Browse files
author
Peter Somogyvari
committed
perf(cmd-api-server): add demonstration of continuous benchmarking
Primary change: --------------- This is the ice-breaker for some work that got stuck related to this issue: hyperledger-cacti#2672 The used benchamking library (benchmark.js) is old but solid and has almost no dependencies which means that we'll be in the clear longer term when it comes to CVEs popping up. The benchmarks added here are very simple and measure the throughput of the API server's Open API spec providing endpoints. The GitHub action that we use is designed to do regression detection and reporting but this is hard to verify before actually putting it in place as we cannot simulate the CI environment's clone on a local environment. The hope is that this change will make it so that if someone tries to make a code change that will lower performance significantly, then we can catch that at the review stage instead of having to find out later. Secondary change: ----------------- 1. Started using tsx in favor of ts-node because it appers to be about 5% faster when looking at the benchmark execution. It also claims to have less problems with ESM compared to ts-node so if this initial trial goes well we could later on decide to swap out ts-node with it project-wide. Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
1 parent 3f36d58 commit 84f5278

6 files changed

Lines changed: 551 additions & 7 deletions

File tree

.cspell.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,21 @@
44
"minWordLength": 4,
55
"allowCompoundWords": true,
66
"words": [
7-
"outsh",
87
"adminpw",
98
"Albertirsa",
109
"ALLFORTX",
11-
"Anoncreds",
1210
"anoncreds",
11+
"Anoncreds",
1312
"ANYFORTX",
14-
"APIV",
1513
"Apim",
14+
"APIV",
1615
"approveformyorg",
17-
"Askar",
1816
"askar",
17+
"Askar",
1918
"Authz",
2019
"authzn",
2120
"AWSSM",
21+
"benchmarkjs",
2222
"Besu",
2323
"Bools",
2424
"brioux",
@@ -47,8 +47,8 @@
4747
"data",
4848
"dclm",
4949
"DHTAPI",
50-
"Dids",
5150
"dids",
51+
"Dids",
5252
"DockerOde",
5353
"ealen",
5454
"ecparams",
@@ -80,8 +80,8 @@
8080
"ipaddress",
8181
"ipfs",
8282
"IPFSHTTP",
83-
"IPLD",
8483
"ipld",
84+
"IPLD",
8585
"Iroha",
8686
"Irohad",
8787
"isready",
@@ -127,6 +127,7 @@
127127
"organisation",
128128
"Orgs",
129129
"ossp",
130+
"outsh",
130131
"parameterizable",
131132
"Postgres",
132133
"proto",

.github/workflows/ci.yaml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,41 @@ jobs:
386386
${{ runner.os }}-yarn-${{ hashFiles('./yarn.lock') }}
387387
388388
- run: ./tools/ci.sh
389+
390+
- name: Ensure .tmp Directory Exists
391+
run: mkdir -p .tmp/benchmark-results/cmd-api-server/
392+
393+
# Download previous benchmark result from cache (if exists)
394+
- name: Download previous benchmark data
395+
uses: actions/cache@v3.3.1
396+
with:
397+
path: .tmp/benchmark-results/cmd-api-server/
398+
key: ${{ runner.os }}-benchmark
399+
400+
- name: Run Benchmarks
401+
working-directory: ./packages/cactus-cmd-api-server/
402+
run: yarn run benchmark
403+
404+
- name: Store benchmark result
405+
uses: benchmark-action/github-action-benchmark@v1.19.2
406+
with:
407+
tool: 'benchmarkjs'
408+
output-file-path: .tmp/benchmark-results/cmd-api-server/run-cmd-api-server-benchmark.ts.log
409+
github-token: ${{ secrets.GITHUB_TOKEN }}
410+
411+
# Where the previous data file is stored
412+
# external-data-json-path: .tmp/benchmark-results/cmd-api-server/benchmark-data.json
413+
414+
# Only push the benchmark results to gh-pages website if we are running on the main branch
415+
# We do not want to clutter the benchmark results with intermediate results from PRs that could be drafts
416+
auto-push: ${{ github.ref == 'refs/heads/main' }}
417+
418+
# Show alert with commit comment on detecting possible performance regression
419+
alert-threshold: '5%'
420+
comment-on-alert: true
421+
fail-on-alert: true
422+
alert-comment-cc-users: '@petermetz'
423+
389424
cactus-cmd-socketio-server:
390425
continue-on-error: false
391426
env:

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"@lerna-lite/version": "3.1.0",
113113
"@openapitools/openapi-generator-cli": "2.7.0",
114114
"@types/adm-zip": "0.5.0",
115+
"@types/benchmark": "2.1.5",
115116
"@types/fs-extra": "9.0.13",
116117
"@types/jest": "29.5.3",
117118
"@types/node": "16.18.41",
@@ -123,6 +124,7 @@
123124
"@typescript-eslint/eslint-plugin": "6.4.0",
124125
"@typescript-eslint/parser": "6.4.0",
125126
"adm-zip": "0.5.10",
127+
"benchmark": "2.1.4",
126128
"buffer": "6.0.3",
127129
"cpy-cli": "4.2.0",
128130
"cross-env": "7.0.3",

packages/cactus-cmd-api-server/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"dist/*"
4444
],
4545
"scripts": {
46+
"benchmark": "tsx ./src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts > ../../.tmp/benchmark-results/cmd-api-server/run-cmd-api-server-benchmark.ts.log",
4647
"codegen": "run-p 'codegen:*'",
4748
"codegen:openapi": "npm run generate-sdk",
4849
"codegen:proto": "run-s proto:openapi proto:protoc-gen-ts",
@@ -97,6 +98,7 @@
9798
"@hyperledger/cactus-plugin-keychain-vault": "2.0.0-alpha.2",
9899
"@hyperledger/cactus-test-tooling": "2.0.0-alpha.2",
99100
"@openapitools/openapi-generator-cli": "2.7.0",
101+
"@types/benchmark": "2.1.5",
100102
"@types/compression": "1.7.4",
101103
"@types/convict": "6.1.1",
102104
"@types/cors": "2.8.12",
@@ -114,11 +116,13 @@
114116
"@types/semver": "7.3.8",
115117
"@types/uuid": "8.3.4",
116118
"@types/xml2js": "0.4.9",
119+
"benchmark": "2.1.4",
117120
"google-protobuf": "3.18.0-rc.2",
118121
"grpc-tools": "1.12.4",
119122
"grpc_tools_node_protoc_ts": "5.3.3",
120123
"http-status-codes": "2.1.4",
121-
"protobufjs": "7.2.5"
124+
"protobufjs": "7.2.5",
125+
"tsx": "4.7.0"
122126
},
123127
"engines": {
124128
"node": ">=18",
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import path from "path";
2+
3+
import * as Benchmark from "benchmark";
4+
import { v4 as uuidv4 } from "uuid";
5+
import type { AuthorizeOptions as SocketIoJwtOptions } from "@thream/socketio-jwt";
6+
import type { Params as ExpressJwtOptions } from "express-jwt";
7+
import { SignJWT, exportSPKI, generateKeyPair } from "jose";
8+
import { RuntimeError } from "run-time-error-cjs";
9+
10+
import { LogLevelDesc } from "@hyperledger/cactus-common";
11+
12+
import { IJoseFittingJwtParams } from "@hyperledger/cactus-common";
13+
import { PluginRegistry } from "@hyperledger/cactus-core";
14+
import { Constants } from "@hyperledger/cactus-core-api";
15+
16+
import {
17+
ApiServer,
18+
ApiServerApiClient,
19+
ApiServerApiClientConfiguration,
20+
AuthorizationProtocol,
21+
ConfigService,
22+
IAuthorizationConfig,
23+
} from "../../../main/typescript/public-api";
24+
25+
import { default_service, empty } from "../../../main/typescript/public-api";
26+
import * as grpc from "@grpc/grpc-js";
27+
28+
const LOG_TAG =
29+
"packages/cactus-cmd-api-server/src/test/typescript/benchmark/run-cmd-api-server-benchmark.ts";
30+
31+
const main = async () => {
32+
const logLevel: LogLevelDesc = "WARN";
33+
34+
const jwtKeyPair = await generateKeyPair("RS256", { modulusLength: 4096 });
35+
const jwtPublicKey = await exportSPKI(jwtKeyPair.publicKey);
36+
const expressJwtOptions: ExpressJwtOptions & IJoseFittingJwtParams = {
37+
algorithms: ["RS256"],
38+
secret: jwtPublicKey,
39+
audience: uuidv4(),
40+
issuer: uuidv4(),
41+
};
42+
const socketIoJwtOptions: SocketIoJwtOptions = {
43+
secret: jwtPublicKey,
44+
algorithms: ["RS256"],
45+
};
46+
47+
const authorizationConfig: IAuthorizationConfig = {
48+
unprotectedEndpointExemptions: [],
49+
expressJwtOptions,
50+
socketIoJwtOptions,
51+
socketIoPath: Constants.SocketIoConnectionPathV1,
52+
};
53+
54+
const pluginsPath = path.join(
55+
__dirname,
56+
"../../../../../../", // walk back up to the project root
57+
".tmp/test/test-cmd-api-server/get-open-api-spec-v1-endpoint_test/", // the dir path from the root
58+
uuidv4(), // then a random directory to ensure proper isolation
59+
);
60+
const pluginManagerOptionsJson = JSON.stringify({ pluginsPath });
61+
62+
const pluginRegistry = new PluginRegistry({ logLevel });
63+
64+
const configService = new ConfigService();
65+
66+
const apiSrvOpts = await configService.newExampleConfig();
67+
apiSrvOpts.logLevel = logLevel;
68+
apiSrvOpts.pluginManagerOptionsJson = pluginManagerOptionsJson;
69+
apiSrvOpts.authorizationProtocol = AuthorizationProtocol.JSON_WEB_TOKEN;
70+
apiSrvOpts.authorizationConfigJson = authorizationConfig;
71+
apiSrvOpts.configFile = "";
72+
apiSrvOpts.apiCorsDomainCsv = "*";
73+
apiSrvOpts.apiPort = 0;
74+
apiSrvOpts.cockpitPort = 0;
75+
apiSrvOpts.grpcPort = 0;
76+
apiSrvOpts.apiTlsEnabled = false;
77+
apiSrvOpts.grpcMtlsEnabled = false;
78+
apiSrvOpts.plugins = [];
79+
80+
const config = await configService.newExampleConfigConvict(apiSrvOpts);
81+
82+
const apiServer = new ApiServer({
83+
config: config.getProperties(),
84+
pluginRegistry,
85+
});
86+
87+
apiServer.initPluginRegistry({ pluginRegistry });
88+
const startResponsePromise = apiServer.start();
89+
90+
const { addressInfoApi, addressInfoGrpc } = await startResponsePromise;
91+
const protocol = apiSrvOpts.apiTlsEnabled ? "https" : "http";
92+
const { address, port } = addressInfoApi;
93+
const apiHost = `${protocol}://${address}:${port}`;
94+
95+
const grpcHost = `${addressInfoGrpc.address}:${addressInfoGrpc.port}`;
96+
97+
const jwtPayload = { name: "Peter", location: "Albertirsa" };
98+
const validJwt = await new SignJWT(jwtPayload)
99+
.setProtectedHeader({ alg: "RS256" })
100+
.setIssuer(expressJwtOptions.issuer)
101+
.setAudience(expressJwtOptions.audience)
102+
.sign(jwtKeyPair.privateKey);
103+
104+
const validBearerToken = `Bearer ${validJwt}`;
105+
106+
const apiClient = new ApiServerApiClient(
107+
new ApiServerApiClientConfiguration({
108+
basePath: apiHost,
109+
baseOptions: { headers: { Authorization: validBearerToken } },
110+
logLevel,
111+
}),
112+
);
113+
114+
const minSamples = 100;
115+
const suite = new Benchmark.Suite({});
116+
const grpcClientInsecureCreds = grpc.credentials.createInsecure();
117+
118+
await new Promise((resolve, reject) => {
119+
suite
120+
.add("cmd-api-server_HTTP_GET_getOpenApiSpecV1", {
121+
defer: true,
122+
minSamples,
123+
fn: async function (deferred: Benchmark.Deferred) {
124+
await apiClient.getOpenApiSpecV1();
125+
deferred.resolve();
126+
},
127+
})
128+
.add("cmd-api-server_gRPC_GetOpenApiSpecV1", {
129+
defer: true,
130+
minSamples,
131+
fn: async function (deferred: Benchmark.Deferred) {
132+
const grpcClient =
133+
new default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient(
134+
grpcHost,
135+
grpcClientInsecureCreds,
136+
);
137+
138+
await new Promise<default_service.org.hyperledger.cactus.cmd_api_server.GetOpenApiSpecV1Response>(
139+
(resolve, reject) => {
140+
const req = new empty.google.protobuf.Empty();
141+
grpcClient.GetOpenApiSpecV1(req, (err3, value) => {
142+
if (err3) {
143+
reject(err3);
144+
} else if (value) {
145+
resolve(value);
146+
} else {
147+
reject(
148+
new RuntimeError("Response object received is falsy."),
149+
);
150+
}
151+
});
152+
},
153+
);
154+
155+
grpcClient.close();
156+
deferred.resolve();
157+
},
158+
})
159+
.on("cycle", (event: { target: unknown }) => {
160+
// Output benchmark result by converting benchmark result to string
161+
// Example line on stdout:
162+
// cmd-api-server_HTTP_GET_getOpenApiSpecV1 x 1,020 ops/sec ±2.25% (177 runs sampled)
163+
console.log(String(event.target));
164+
})
165+
.on("complete", function () {
166+
resolve(suite);
167+
})
168+
.on("error", (ex: unknown) => {
169+
console.log("Benchmark.js crash: ", ex);
170+
reject(ex);
171+
})
172+
.run();
173+
});
174+
175+
await apiServer.shutdown();
176+
};
177+
178+
main().catch((ex: unknown) => {
179+
console.error("%s process crashed with:", LOG_TAG, ex);
180+
process.exit(1);
181+
});

0 commit comments

Comments
 (0)