-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test: add key derivation benchmark tests and docs (#563)
* tests: benchamrk for key derivation * tests: vault time tests * tests: extract common test template * tests: fix stats log * docs: add document derivation results * docs: fix typo * docs: add jdk version
- Loading branch information
Showing
2 changed files
with
204 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Key derivation benchmark | ||
|
||
This document provides a performance benchmark of a key derivation used inside the PRISM agent | ||
in comparison with a key retrieval from HashiCorp Vault. It should provide a baseline for | ||
future decisions in managing the key material on PRISM agent. | ||
|
||
## Test setup | ||
|
||
### Environment | ||
|
||
__System information__ | ||
- Platform: Linux (6.3.7) | ||
- CPU: AMD Ryzen 7 PRO 6850U | ||
- Memory: 30856MiB | ||
- JDK version: OpenJDK 11.0.18 | ||
- SBT version: 1.8.0 | ||
|
||
__JVM options__ | ||
- Xmx:4G | ||
|
||
The tests can be run by running the `io.iohk.atala.agent.walletapi.benchmark.KeyDerivation`. | ||
The tests are being ignored to avoid running them on CI. When running locally, | ||
the ignore aspect should be removed and the test can be run by | ||
|
||
```bash | ||
sbt prismAgentWalletAPI/'testOnly -- -tag benchmark' | ||
``` | ||
|
||
## Scenario | ||
|
||
### Key Derivation | ||
|
||
__Setup__ | ||
|
||
1. Warm-up JVM by running key derivation for 10k iterations | ||
2. Running key derivation for 50k iterations with `N` parallelism | ||
3. Measure the average, maximum and percentile of execution time (p50, p90, p99). | ||
The measurements consider derivation execution time of a single key. | ||
|
||
__Results__ | ||
|
||
Duration in *microseconds*. Lower is better. | ||
|
||
| Parallelism | Avg | P50 | P90 | P99 | Max | | ||
|-------------|---------|--------|---------|----------|----------| | ||
| 1 | 406.97 | 402.91 | 418.56 | 437.83 | 4550.13 | | ||
| 8 | 499.59 | 475.20 | 544.83 | 826.22 | 2928.68 | | ||
| 16 | 877.71 | 831.53 | 931.34 | 2278.10 | 10306.08 | | ||
| 32 | 1772.57 | 821.06 | 1327.90 | 20331.60 | 61460.31 | | ||
|
||
### Key retrieval from Vault | ||
|
||
__Setup__ | ||
|
||
1. Warm-up JVM, Vault and their connections by setting/getting 100 keys | ||
2. Running querying the KV from Vault for 50k iterations with `N` parallelism | ||
3. Measure the average, maximum and percentile of execution duration (p50, p90, p99). | ||
The measurements consider the query time and serialization time of a single key. | ||
|
||
Note: Vault server runs in a docker container on the same machine using in-memory storage. | ||
So the setup may yield optimistic results. | ||
|
||
__Results__ | ||
|
||
Duration in *microseconds*. Lower is better. | ||
|
||
| Parallelism | Avg | P50 | P90 | P99 | Max | | ||
|-------------|---------|---------|---------|---------|----------| | ||
| 1 | 575.62 | 535.61 | 703.44 | 798.64 | 11388.56 | | ||
| 8 | 717.67 | 666.64 | 913.04 | 1424.77 | 10854.26 | | ||
| 16 | 1193.11 | 1098.47 | 1690.94 | 2818.88 | 11410.41 | | ||
| 32 | 2379.26 | 2146.38 | 3954.72 | 6376.56 | 22210.13 | |
132 changes: 132 additions & 0 deletions
132
...ice/wallet-api/src/test/scala/io/iohk/atala/agent/walletapi/benchmark/KeyDerivation.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
package io.iohk.atala.agent.walletapi.benchmark | ||
|
||
import zio.* | ||
import zio.test.* | ||
import zio.test.Assertion.* | ||
import io.iohk.atala.agent.walletapi.crypto.Apollo | ||
import io.iohk.atala.castor.core.model.did.EllipticCurve | ||
import io.iohk.atala.shared.models.HexString | ||
import io.iohk.atala.agent.walletapi.crypto.DerivationPath | ||
import io.iohk.atala.agent.walletapi.vault.VaultKVClientImpl | ||
import io.iohk.atala.agent.walletapi.vault.VaultKVClient | ||
import io.iohk.atala.test.container.VaultTestContainerSupport | ||
import io.iohk.atala.shared.models.Base64UrlString | ||
|
||
object KeyDerivation extends ZIOSpecDefault, VaultTestContainerSupport { | ||
|
||
private val seedHex = "00" * 64 | ||
private val seed = HexString.fromStringUnsafe(seedHex).toByteArray | ||
|
||
override def spec = suite("Key derivation benchamrk")( | ||
deriveKeyBenchmark.provide(Apollo.prism14Layer), | ||
queryKeyBenchmark.provide(vaultKvClientLayer, Apollo.prism14Layer) | ||
) @@ TestAspect.sequential @@ TestAspect.timed @@ TestAspect.tag("benchmark") @@ TestAspect.ignore | ||
|
||
private val deriveKeyBenchmark = suite("Key derivation benchmark")( | ||
benchamrkKeyDerivation(1), | ||
benchamrkKeyDerivation(8), | ||
benchamrkKeyDerivation(16), | ||
benchamrkKeyDerivation(32), | ||
) @@ TestAspect.before(deriveKeyWarmUp()) | ||
|
||
private val queryKeyBenchmark = suite("Query key benchmark - vault storage")( | ||
benchmarkVaultQuery(1), | ||
benchmarkVaultQuery(8), | ||
benchmarkVaultQuery(16), | ||
benchmarkVaultQuery(32), | ||
) @@ TestAspect.before(vaultWarmUp()) | ||
|
||
private def benchamrkKeyDerivation(parallelism: Int) = { | ||
test(s"derive 50000 keys - $parallelism parallelism") { | ||
for { | ||
apollo <- ZIO.service[Apollo] | ||
durationList <- ZIO | ||
.foreachPar(1 to 50_000) { i => | ||
Live.live { | ||
apollo.ecKeyFactory | ||
.deriveKeyPair(EllipticCurve.SECP256K1, seed)(derivationPath(keyIndex = i): _*) | ||
.timed | ||
.map(_._1) | ||
} | ||
} | ||
.withParallelism(parallelism) | ||
_ <- logStats(durationList) | ||
} yield assertCompletes | ||
} | ||
} | ||
|
||
private def benchmarkVaultQuery(parallelism: Int) = { | ||
test(s"query 50000 keys - $parallelism parallelism") { | ||
for { | ||
vaultClient <- ZIO.service[VaultKVClient] | ||
apollo <- ZIO.service[Apollo] | ||
keyPair <- apollo.ecKeyFactory.generateKeyPair(EllipticCurve.SECP256K1) | ||
encodedKey = Base64UrlString.fromByteArray(keyPair.privateKey.encode).toString() | ||
_ <- ZIO | ||
.foreach(1 to 50_000) { i => vaultClient.set(s"secret/did/prism/key-$i", Map("value" -> encodedKey)) } | ||
durationList <- ZIO | ||
.foreachPar(1 to 50_000) { i => | ||
Live.live { | ||
vaultClient | ||
.get(s"secret/did/prism/key-$i") | ||
.flatMap { encodedKey => | ||
val encodedBytes = | ||
Base64UrlString.fromString(encodedKey.get.get("value").get).toOption.get.toByteArray | ||
ZIO.fromTry(apollo.ecKeyFactory.privateKeyFromEncoded(EllipticCurve.SECP256K1, encodedBytes)) | ||
} | ||
.timed | ||
.map(_._1) | ||
} | ||
} | ||
.withParallelism(parallelism) | ||
_ <- logStats(durationList) | ||
} yield assertCompletes | ||
} | ||
} | ||
|
||
private def deriveKeyWarmUp(n: Int = 10000) = { | ||
for { | ||
_ <- ZIO.debug("running key derivation warm-up") | ||
apollo <- ZIO.service[Apollo] | ||
_ <- ZIO | ||
.foreach(1 to n) { i => | ||
apollo.ecKeyFactory | ||
.deriveKeyPair(EllipticCurve.SECP256K1, seed)(derivationPath(keyIndex = i): _*) | ||
} | ||
} yield () | ||
} | ||
|
||
private def vaultWarmUp(n: Int = 100) = { | ||
for { | ||
vaultClient <- ZIO.service[VaultKVClient] | ||
_ <- ZIO.debug("running vault warm-up") | ||
_ <- ZIO.foreach(1 to n) { i => | ||
vaultClient.set(s"secret/warm-up/key-$i", Map("hello" -> "world")) | ||
} | ||
_ <- ZIO.foreach(1 to n) { i => | ||
vaultClient.get(s"secret/warm-up/key-$i") | ||
} | ||
} yield () | ||
} | ||
|
||
private def derivationPath(keyIndex: Int = 0): Seq[DerivationPath] = { | ||
Seq( | ||
DerivationPath.Hardened(0x1d), | ||
DerivationPath.Hardened(0), | ||
DerivationPath.Hardened(0), | ||
DerivationPath.Hardened(keyIndex), | ||
) | ||
} | ||
|
||
private def logStats(durationList: Seq[Duration]) = { | ||
val n = durationList.length | ||
val sortedDurationInMicro = durationList.sorted.map(_.toNanos() / 1000.0) | ||
val avg = sortedDurationInMicro.sum / n | ||
val p50 = sortedDurationInMicro.apply((0.50 * n).toInt) | ||
val p75 = sortedDurationInMicro.apply((0.75 * n).toInt) | ||
val p90 = sortedDurationInMicro.apply((0.90 * n).toInt) | ||
val p99 = sortedDurationInMicro.apply((0.99 * n).toInt) | ||
val max = sortedDurationInMicro.last | ||
ZIO.debug(s"execution time in us. avg: $avg | p50: $p50 | p90: $p90 | p99: $p99 | max: $max") | ||
} | ||
} |