Skip to content

Commit 8824f42

Browse files
authored
ci: add JS benchmark runner (#4978)
1 parent 8edfff5 commit 8824f42

18 files changed

+594
-20
lines changed

book/src/02_development/01_tools.md

+6
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ Only those requests will be collected that can be successfully deserialized.
4747
4. The scenario file will be written to the current working directory with the desired file name prefix.
4848
5. Optionally, compress the scenario file `gzip -k <SCENARIO_FILE>`. (The `-k` option preserves the original file, omit it if you want it deleted.)
4949

50+
## Rust runner
51+
5052
```bash
5153

5254
### Run scenario
@@ -59,3 +61,7 @@ cargo run --bin tools --release scenario <PATH_TO_SCENARIO_FILE>
5961
The scenario runner supports both compressed and uncompressed scenario files.
6062

6163
The reported running time excludes reading the requests from disk and parsing them.
64+
65+
## JS runner
66+
67+
Please see the [readme](../../../crates/tools/js/benchmark/README.md) for instructions.

crates/edr_eth/src/signature.rs

+13
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ pub fn secret_key_from_str(secret_key: &str) -> Result<SecretKey, SignatureError
5959
SecretKey::from_bytes(&secret_key).map_err(SignatureError::EllipticCurveError)
6060
}
6161

62+
/// Converts a secret key to a 0x-prefixed hex string.
63+
pub fn secret_key_to_str(secret_key: &SecretKey) -> String {
64+
format!("0x{}", hex::encode(secret_key.to_bytes().as_slice()))
65+
}
66+
6267
/// An error involving a signature.
6368
#[derive(Debug)]
6469
#[cfg_attr(feature = "std", derive(thiserror::Error))]
@@ -451,4 +456,12 @@ mod tests {
451456
verify(message, hashed_message);
452457
verify(hashed_message, hashed_message);
453458
}
459+
460+
#[test]
461+
fn test_from_str_to_str_secret_key() {
462+
let secret_key_str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
463+
let secret_key = secret_key_from_str(secret_key_str).unwrap();
464+
let secret_key_str_result = secret_key_to_str(&secret_key);
465+
assert_eq!(secret_key_str, secret_key_str_result);
466+
}
454467
}

crates/edr_provider/src/config.rs

+21-19
Original file line numberDiff line numberDiff line change
@@ -87,31 +87,33 @@ pub struct ProviderConfig {
8787
#[derive(Debug, Clone, Deserialize, Serialize)]
8888
pub struct AccountConfig {
8989
/// the secret key of the account
90-
#[serde(
91-
serialize_with = "serialize_secret_key",
92-
deserialize_with = "deserialize_secret_key"
93-
)]
90+
#[serde(with = "secret_key_serde")]
9491
pub secret_key: k256::SecretKey,
9592
/// the balance of the account
9693
pub balance: U256,
9794
}
9895

99-
fn serialize_secret_key<S>(secret_key: &k256::SecretKey, serializer: S) -> Result<S::Ok, S::Error>
100-
where
101-
S: serde::Serializer,
102-
{
103-
let s = secret_key
104-
.to_sec1_pem(k256::pkcs8::LineEnding::LF)
105-
.map_err(serde::ser::Error::custom)?;
106-
serializer.serialize_str(&s)
107-
}
96+
mod secret_key_serde {
97+
use edr_eth::signature::{secret_key_from_str, secret_key_to_str};
98+
use serde::Deserialize;
99+
100+
pub(super) fn serialize<S>(
101+
secret_key: &k256::SecretKey,
102+
serializer: S,
103+
) -> Result<S::Ok, S::Error>
104+
where
105+
S: serde::Serializer,
106+
{
107+
serializer.serialize_str(&secret_key_to_str(secret_key))
108+
}
108109

109-
fn deserialize_secret_key<'de, D>(deserializer: D) -> Result<k256::SecretKey, D::Error>
110-
where
111-
D: serde::Deserializer<'de>,
112-
{
113-
let s = String::deserialize(deserializer)?;
114-
k256::SecretKey::from_sec1_pem(&s).map_err(serde::de::Error::custom)
110+
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<k256::SecretKey, D::Error>
111+
where
112+
D: serde::Deserializer<'de>,
113+
{
114+
let s = <&str as Deserialize>::deserialize(deserializer)?;
115+
secret_key_from_str(s).map_err(serde::de::Error::custom)
116+
}
115117
}
116118

117119
impl Default for MemPoolConfig {

crates/tools/js/benchmark/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

crates/tools/js/benchmark/README.md

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# JS Benchmark Runner
2+
3+
## Run
4+
5+
To run:
6+
7+
```shell
8+
pnpm install
9+
pnpm run benchmark
10+
```
11+
12+
The measurements will be printed to stdout as machine-readable json and to stderr as human-readable output.
13+
14+
## Grep
15+
16+
It's possible to grep the output to run a specific scenario:
17+
18+
```shell
19+
npm run benchmark -- --grep seaport
20+
```

crates/tools/js/benchmark/index.js

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
const path = require("path");
2+
const fs = require("fs");
3+
const readline = require("readline");
4+
const zlib = require("zlib");
5+
6+
const { _ } = require("lodash");
7+
8+
const {
9+
createHardhatNetworkProvider,
10+
} = require("hardhat/internal/hardhat-network/provider/provider");
11+
12+
const SCENARIOS_DIR = "../../scenarios/";
13+
14+
function usage() {
15+
console.error("Usage: node index.js [--grep|-g <pattern>]");
16+
process.exit(1);
17+
}
18+
19+
async function main() {
20+
const numArgs = process.argv.length;
21+
22+
if (numArgs !== 2 && numArgs !== 4) {
23+
usage();
24+
}
25+
26+
let grep = undefined;
27+
if (numArgs === 4) {
28+
if (process.argv[2] !== "--grep" && process.argv[2] !== "-g") {
29+
usage();
30+
}
31+
32+
grep = process.argv[3];
33+
}
34+
35+
const result = {};
36+
const scenariosDir = path.join(__dirname, SCENARIOS_DIR);
37+
38+
// List files in scenarios directory
39+
const scenarioFiles = fs.readdirSync(scenariosDir);
40+
scenarioFiles.sort();
41+
let totalTime = 0;
42+
let totalFailures = 0;
43+
for (let scenarioFile of scenarioFiles) {
44+
if (grep && !scenarioFile.includes(grep)) {
45+
continue;
46+
}
47+
// Get the filename from the path
48+
const scenarioResult = await runScenario(
49+
path.join(scenariosDir, scenarioFile)
50+
);
51+
totalTime += scenarioResult.result.timeMs;
52+
totalFailures += scenarioResult.result.failures.length;
53+
result[scenarioResult.name] = scenarioResult.result;
54+
}
55+
56+
console.log(JSON.stringify(result));
57+
58+
// Log info to stderr so that it doesn't pollute stdout where we write the result
59+
console.error(
60+
`Total time ${
61+
Math.round(100 * (totalTime / 1000)) / 100
62+
} seconds with ${totalFailures} failures.`
63+
);
64+
65+
process.exit(0);
66+
}
67+
68+
async function runScenario(scenarioPath) {
69+
const { config, requests } = await loadScenario(scenarioPath);
70+
const name = path.basename(scenarioPath).split(".")[0];
71+
console.error(`Running ${name} scenario`);
72+
73+
const start = performance.now();
74+
75+
const provider = await createHardhatNetworkProvider(config.providerConfig, {
76+
enabled: config.loggerEnabled,
77+
});
78+
79+
const failures = [];
80+
81+
for (let i = 0; i < requests.length; i += 1) {
82+
try {
83+
await provider.request(requests[i]);
84+
} catch (e) {
85+
failures.push(i);
86+
}
87+
}
88+
89+
const timeMs = performance.now() - start;
90+
91+
console.error(
92+
`${name} finished in ${
93+
Math.round(100 * (timeMs / 1000)) / 100
94+
} seconds with ${failures.length} failures.`
95+
);
96+
97+
return {
98+
name,
99+
result: {
100+
timeMs,
101+
failures,
102+
},
103+
};
104+
}
105+
106+
async function loadScenario(path) {
107+
const result = {
108+
requests: [],
109+
};
110+
let i = 0;
111+
for await (const line of readFile(path)) {
112+
const parsed = JSON.parse(line);
113+
if (i === 0) {
114+
result.config = preprocessConfig(parsed);
115+
} else {
116+
result.requests.push(parsed);
117+
}
118+
i += 1;
119+
}
120+
return result;
121+
}
122+
123+
function preprocessConfig(config) {
124+
// From https://stackoverflow.com/a/59771233
125+
const camelize = (obj) =>
126+
_.transform(obj, (acc, value, key, target) => {
127+
const camelKey = _.isArray(target) ? key : _.camelCase(key);
128+
129+
acc[camelKey] = _.isObject(value) ? camelize(value) : value;
130+
});
131+
config = camelize(config);
132+
133+
// EDR serializes None as null to json, but Hardhat expects it to be undefined
134+
const removeNull = (obj) =>
135+
_.transform(obj, (acc, value, key) => {
136+
if (_.isObject(value)) {
137+
acc[key] = removeNull(value);
138+
} else if (!_.isNull(value)) {
139+
acc[key] = value;
140+
}
141+
});
142+
config = removeNull(config);
143+
144+
config.providerConfig.initialDate = new Date(
145+
config.providerConfig.initialDate.secsSinceEpoch * 1000
146+
);
147+
148+
config.providerConfig.hardfork = normalizeHardfork(
149+
config.providerConfig.hardfork
150+
);
151+
152+
// "accounts" in EDR are "genesisAccounts" in Hardhat
153+
if (Object.keys(config.providerConfig.genesisAccounts).length !== 0) {
154+
throw new Error("Genesis accounts are not supported");
155+
}
156+
config.providerConfig.genesisAccounts = config.providerConfig.accounts.map(
157+
({ balance, secretKey }) => {
158+
return { balance, privateKey: secretKey };
159+
}
160+
);
161+
delete config.providerConfig.accounts;
162+
163+
config.providerConfig.automine = config.providerConfig.mining.autoMine;
164+
config.providerConfig.mempoolOrder =
165+
config.providerConfig.mining.memPool.order.toLowerCase();
166+
config.providerConfig.intervalMining =
167+
config.providerConfig.mining.interval ?? 0;
168+
delete config.providerConfig.mining;
169+
170+
config.providerConfig.throwOnCallFailures =
171+
config.providerConfig.bailOnCallFailure;
172+
delete config.providerConfig.bailOnCallFailure;
173+
config.providerConfig.throwOnTransactionFailures =
174+
config.providerConfig.bailOnTransactionFailure;
175+
delete config.providerConfig.bailOnTransactionFailure;
176+
177+
let chains = new Map();
178+
for (let key of Object.keys(config.providerConfig.chains)) {
179+
const hardforkHistory = new Map();
180+
const hardforks = config.providerConfig.chains[key].hardforks;
181+
for (let [blockNumber, hardfork] of hardforks) {
182+
hardforkHistory.set(normalizeHardfork(hardfork), blockNumber);
183+
}
184+
chains.set(Number(key), { hardforkHistory });
185+
}
186+
config.providerConfig.chains = chains;
187+
188+
if (!_.isUndefined(config.providerConfig.fork)) {
189+
config.providerConfig.forkConfig = config.providerConfig.fork;
190+
delete config.providerConfig.fork;
191+
}
192+
193+
config.providerConfig.minGasPrice = BigInt(config.providerConfig.minGasPrice);
194+
195+
return config;
196+
}
197+
198+
function normalizeHardfork(hardfork) {
199+
hardfork = _.camelCase(hardfork.toLowerCase());
200+
if (hardfork === "frontier") {
201+
hardfork = "chainstart";
202+
} else if (hardfork === "daoFork") {
203+
hardfork = "dao";
204+
} else if (hardfork == "tangerine") {
205+
hardfork = "tangerineWhistle";
206+
}
207+
return hardfork;
208+
}
209+
210+
// From https://stackoverflow.com/a/65015455/2650622
211+
function readFile(path) {
212+
let stream = fs.createReadStream(path);
213+
214+
if (/\.gz$/i.test(path)) {
215+
stream = stream.pipe(zlib.createGunzip());
216+
}
217+
218+
return readline.createInterface({
219+
input: stream,
220+
crlfDelay: Infinity,
221+
});
222+
}
223+
224+
main().catch((error) => {
225+
console.error(error);
226+
process.exit(1);
227+
});
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "benchmark",
3+
"version": "1.0.0",
4+
"private": true,
5+
"description": "",
6+
"main": "index.js",
7+
"scripts": {
8+
"benchmark": "node index.js",
9+
"prebenchmark": "cd ../../../edr_napi/ && pnpm build && cd ../../packages/hardhat-core/ && pnpm build"
10+
},
11+
"keywords": [],
12+
"author": "",
13+
"license": "ISC",
14+
"dependencies": {
15+
"hardhat": "workspace:^",
16+
"lodash": "^4.17.11",
17+
"tsx": "^4.7.1"
18+
}
19+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2020",
4+
"module": "commonjs",
5+
"esModuleInterop": true,
6+
"forceConsistentCasingInFileNames": true,
7+
"strict": true,
8+
"skipLibCheck": true,
9+
"resolveJsonModule": true
10+
}
11+
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
52.8 KB
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)