chore(js): add benchmark for unsafe coop context

This commit is contained in:
Nicolas Sarlin
2025-12-29 15:00:24 +01:00
parent ca2a79f1fb
commit d3abc783b3
16 changed files with 5020 additions and 7 deletions

View File

@@ -153,6 +153,12 @@ jobs:
env:
BROWSER: ${{ matrix.browser }}
- name: Run benchmarks (unsafe coop)
run: |
make bench_web_js_api_unsafe_coop_"${BROWSER}"_ci
env:
BROWSER: ${{ matrix.browser }}
- name: Parse results
run: |
make parse_wasm_benchmarks

View File

@@ -29,6 +29,7 @@ WASM_PACK_VERSION="0.13.1"
WASM_BINDGEN_VERSION:=$(shell cargo tree --target wasm32-unknown-unknown -e all --prefix none | grep "wasm-bindgen v" | head -n 1 | cut -d 'v' -f2)
WEB_RUNNER_DIR=web-test-runner
WEB_SERVER_DIR=tfhe/web_wasm_parallel_tests
WEB_SERVER_DIR_UNSAFE_COOP=tfhe/web_wasm_unsafe_coop_tests
TYPOS_VERSION=1.39.0
ZIZMOR_VERSION=1.16.2
# This is done to avoid forgetting it, we still precise the RUSTFLAGS in the commands to be able to
@@ -1598,6 +1599,49 @@ bench_web_js_api_parallel_firefox_ci: setup_venv
nvm use $(NODE_VERSION) && \
$(MAKE) bench_web_js_api_parallel_firefox
# This is an internal target, not meant to be called on its own.
run_web_js_api_unsafe_coop: build_web_js_api setup_venv
cd $(WEB_SERVER_DIR_UNSAFE_COOP) && npm install && npm run build
source venv/bin/activate && \
python ci/webdriver.py \
--browser-path $(browser_path) \
--driver-path $(driver_path) \
--browser-kind $(browser_kind) \
--server-cmd "npm run server" \
--server-workdir "$(WEB_SERVER_DIR_UNSAFE_COOP)" \
--index-path "$(WEB_SERVER_DIR_UNSAFE_COOP)/index.html" \
--id-pattern $(filter)
bench_web_js_api_unsafe_coop_chrome: browser_path = "$(WEB_RUNNER_DIR)/chrome/chrome-linux64/chrome"
bench_web_js_api_unsafe_coop_chrome: driver_path = "$(WEB_RUNNER_DIR)/chrome/chromedriver-linux64/chromedriver"
bench_web_js_api_unsafe_coop_chrome: browser_kind = chrome
bench_web_js_api_unsafe_coop_chrome: filter = Bench
.PHONY: bench_web_js_api_unsafe_coop_chrome # Run benchmarks for the web wasm api without cross-origin isolation
bench_web_js_api_unsafe_coop_chrome: run_web_js_api_unsafe_coop
.PHONY: bench_web_js_api_unsafe_coop_chrome_ci # Run benchmarks for the web wasm api without cross-origin isolation
bench_web_js_api_unsafe_coop_chrome_ci: setup_venv
source ~/.nvm/nvm.sh && \
nvm install $(NODE_VERSION) && \
nvm use $(NODE_VERSION) && \
$(MAKE) bench_web_js_api_unsafe_coop_chrome
bench_web_js_api_unsafe_coop_firefox: browser_path = "$(WEB_RUNNER_DIR)/firefox/firefox/firefox"
bench_web_js_api_unsafe_coop_firefox: driver_path = "$(WEB_RUNNER_DIR)/firefox/geckodriver"
bench_web_js_api_unsafe_coop_firefox: browser_kind = firefox
bench_web_js_api_unsafe_coop_firefox: filter = Bench
.PHONY: bench_web_js_api_unsafe_coop_firefox # Run benchmarks for the web wasm api without cross-origin isolation
bench_web_js_api_unsafe_coop_firefox: run_web_js_api_unsafe_coop
.PHONY: bench_web_js_api_unsafe_coop_firefox_ci # Run benchmarks for the web wasm api without cross-origin isolation
bench_web_js_api_unsafe_coop_firefox_ci: setup_venv
source ~/.nvm/nvm.sh && \
nvm install $(NODE_VERSION) && \
nvm use $(NODE_VERSION) && \
$(MAKE) bench_web_js_api_unsafe_coop_firefox
.PHONY: bench_hlapi # Run benchmarks for integer operations
bench_hlapi: install_rs_check_toolchain
RUSTFLAGS="$(RUSTFLAGS)" __TFHE_RS_BENCH_BIT_SIZES_SET=$(BIT_SIZES_SET) \

View File

@@ -367,6 +367,8 @@ def dump_benchmark_results(results, browser_kind):
"""
Dump as JSON benchmark results into a file.
If `results` is an empty dict then this function is a no-op.
If the file already exists, new results are merged with existing ones,
overwriting keys that already exist.
:param results: benchmark results as :class:`dict`
:param browser_kind: browser as :class:`BrowserKind`
@@ -376,7 +378,15 @@ def dump_benchmark_results(results, browser_kind):
key.replace("mean", "_".join((browser_kind.name, "mean"))): val
for key, val in results.items()
}
pathlib.Path("tfhe-benchmark/wasm_benchmark_results.json").write_text(json.dumps(results))
results_path = pathlib.Path("tfhe-benchmark/wasm_benchmark_results.json")
existing_results = {}
if results_path.exists():
try:
existing_results = json.loads(results_path.read_text())
except json.JSONDecodeError:
pass
existing_results.update(results)
results_path.write_text(json.dumps(existing_results))
def start_web_server(

View File

@@ -26,6 +26,7 @@
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
@@ -392,6 +393,7 @@
"integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/template": "^7.25.0",
"@babel/types": "^7.25.6"
@@ -2131,7 +2133,6 @@
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2391,7 +2392,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
@@ -2714,7 +2714,8 @@
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/core-js-compat": {
"version": "3.38.1",
@@ -3003,6 +3004,7 @@
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6.9.0"
}
@@ -3306,6 +3308,7 @@
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"json5": "lib/cli.js"
},
@@ -3879,7 +3882,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -4399,7 +4401,6 @@
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.12.1",
@@ -4447,7 +4448,6 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",

View File

@@ -0,0 +1,2 @@
dist/
node_modules/

View File

@@ -0,0 +1,4 @@
dist/
node_modules/
pkg/
test/benchmark_results/

View File

@@ -0,0 +1,15 @@
.PHONY: run_server # Build and run Node server
run_server:
npm install
npm run build
npm run server
.PHONY: fmt # Format Javascript code
fmt:
npm install
npm run format
.PHONY: check_fmt # Check Javascript code format
check_fmt:
npm install
npm run check-format

View File

@@ -0,0 +1,3 @@
const presets = [["@babel/preset-env"]];
module.exports = { presets };

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

View File

@@ -0,0 +1,47 @@
<!doctype html>
<title>TFHE-RS Web Wasm Demo</title>
<body>
<style>
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<div>
<!-- Use input `max` attribute to specify a custom timeout in seconds for a test/bench
Default timeout is available in ci/webdriver.py under variable name `case_timeout_seconds`
-->
<input
type="button"
id="compactPublicKeyZeroKnowledgeBench"
value="Compact ZK Bench"
max="3600"
disabled
/>
<input type="checkbox" id="testSuccess" disabled />
<label for="testSuccess"> TestSuccess </label><br />
<input type="text" id="benchmarkResults" disabled />
<label for="benchmarkResults"> BenchmarkResults </label><br />
<div id="loader" class="loader" hidden></div>
</div>
<script type="module" src="index.js"></script>
</body>

View File

@@ -0,0 +1,65 @@
import * as Comlink from "comlink";
function setButtonsDisabledState(buttonIds, state) {
for (let id of buttonIds) {
let btn = document.getElementById(id);
if (btn) {
btn.disabled = state;
}
}
}
async function setup() {
const worker = new Worker(new URL("worker.js", import.meta.url), {
type: "module",
});
const demos = await Comlink.wrap(worker).demos;
const demoNames = [
"compactPublicKeyZeroKnowledgeBench",
];
function setupBtn(id) {
// Handlers are named in the same way as buttons.
let fn = demos[id];
let button = document.getElementById(id);
if (button === null) {
console.error(`button with id: ${id} not found`);
return null;
}
// Assign onclick handler + enable the button.
Object.assign(button, {
onclick: async () => {
document.getElementById("loader").hidden = false;
document.getElementById("testSuccess").checked = false;
setButtonsDisabledState(demoNames, true);
console.log(`Running: ${id}`);
try {
let results = await fn();
document.getElementById("testSuccess").checked = true;
if (results !== undefined) {
document.getElementById("benchmarkResults").value =
JSON.stringify(results);
}
} catch (error) {
console.error(`Test Failed: ${error}`);
document.getElementById("testSuccess").checked = false;
}
document.getElementById("loader").hidden = true;
setButtonsDisabledState(demoNames, false);
},
disabled: false,
});
return button;
}
for (let demo of demoNames) {
setupBtn(demo);
}
}
setup();

View File

@@ -0,0 +1,15 @@
const secs = 3600; // 60 Minutes
const config = {
verbose: true,
testTimeout: secs * 1000,
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?|tsx?|ts?)$",
transform: {
"^.+\\.jsx?$": "babel-jest",
"^.+\\.mjs$": "babel-jest",
},
testPathIgnorePatterns: ["<rootDir>/build/", "<rootDir>/node_modules/"],
moduleFileExtensions: ["js", "jsx", "mjs"],
};
module.exports = config;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
{
"name": "tfhe-wasm-unsafe-coop",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "cp -r ../../tfhe/pkg ./ && webpack build ./index.js --mode production -o dist --output-filename index.js && cp index.html dist/ && cp favicon.ico dist/",
"server": "serve --config ../serve.json dist/",
"format": "prettier . --write",
"check-format": "prettier . --check"
},
"author": "",
"license": "BSD-3-Clause-Clear",
"devDependencies": {
"@babel/preset-env": "^7.25.4",
"prettier": "^3.3.3",
"serve": "^14.2.5",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"comlink": "^4.4.1",
"wasm-feature-detect": "^1.6.2"
}
}

View File

@@ -0,0 +1,11 @@
{
"headers": [
{
"source": "**/*.@(js|html)",
"headers": [
{ "key": "Cross-Origin-Embedder-Policy", "value": "unsafe-none" },
{ "key": "Cross-Origin-Opener-Policy", "value": "unsafe-none" }
]
}
]
}

View File

@@ -0,0 +1,133 @@
import * as Comlink from "comlink";
import init, {
init_panic_hook,
shortint_params_name,
ShortintParametersName,
ShortintParameters,
TfheClientKey,
TfheCompactPublicKey,
TfheConfigBuilder,
ZkComputeLoad,
CompactPkeCrs,
ProvenCompactCiphertextList,
ShortintCompactPublicKeyEncryptionParameters,
ShortintCompactPublicKeyEncryptionParametersName,
} from "./pkg/tfhe.js";
const U64_MAX = BigInt("0xffffffffffffffff");
async function compactPublicKeyZeroKnowledgeBench() {
let params_to_bench = [
{
zk_scheme: "ZKV2",
name: shortint_params_name(
ShortintParametersName.PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128,
),
block_params: new ShortintParameters(
ShortintParametersName.PARAM_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128,
),
casting_params: new ShortintCompactPublicKeyEncryptionParameters(
ShortintCompactPublicKeyEncryptionParametersName.PARAM_PKE_MESSAGE_2_CARRY_2_KS_PBS_TUNIFORM_2M128,
),
},
];
let bench_results = {};
for (const params of params_to_bench) {
let block_params_name = params.name;
let block_params = params.block_params;
let casting_params = params.casting_params;
let config = TfheConfigBuilder.default()
.use_custom_parameters(block_params)
.use_dedicated_compact_public_key_parameters(casting_params)
.build();
let clientKey = TfheClientKey.generate(config);
let publicKey = TfheCompactPublicKey.new(clientKey);
const bench_loops = 5; // The computation is expensive
let load_choices = [ZkComputeLoad.Proof, ZkComputeLoad.Verify];
const load_to_str = {
[ZkComputeLoad.Proof]: "compute_load_proof",
[ZkComputeLoad.Verify]: "compute_load_verify",
};
// Proof configuration:
let proof_configs = [
{ crs_bit_size: 64, bits_to_encrypt: [64] },
// 64 * 4 is a production use-case
{ crs_bit_size: 2048, bits_to_encrypt: [64, 4 * 64, 2048] },
{ crs_bit_size: 4096, bits_to_encrypt: [4096] },
];
for (const proof_config of proof_configs) {
console.log("Start CRS generation");
console.time("CRS generation");
let crs = CompactPkeCrs.from_config(config, proof_config["crs_bit_size"]);
console.timeEnd("CRS generation");
// 320 bits is a use case we have, 8 bits per byte
const metadata = new Uint8Array(320 / 8);
crypto.getRandomValues(metadata);
for (const bits_to_encrypt of proof_config["bits_to_encrypt"]) {
let encrypt_count = bits_to_encrypt / 64;
let inputs = Array.from(Array(encrypt_count).keys()).map(
(_) => U64_MAX,
);
for (const loadChoice of load_choices) {
let timing = 0;
for (let i = 0; i < bench_loops; i++) {
console.time("Loop " + i);
let compact_list_builder =
ProvenCompactCiphertextList.builder(publicKey);
for (let j = 0; j < encrypt_count; j++) {
compact_list_builder.push_u64(inputs[j]);
}
const start = performance.now();
let list = compact_list_builder.build_with_proof_packed(
crs,
metadata,
loadChoice,
);
const end = performance.now();
console.timeEnd("Loop " + i);
timing += end - start;
}
const mean = timing / bench_loops;
const common_bench_str =
"compact_fhe_uint_proven_encryption_unsafe_coop_" +
params.zk_scheme +
"_" +
bits_to_encrypt +
"_bits_packed_" +
proof_config["crs_bit_size"] +
"_bits_crs_" +
load_to_str[loadChoice];
const bench_str_1 = common_bench_str + "_mean_" + block_params_name;
console.log(bench_str_1, ": ", mean, " ms");
bench_results[bench_str_1] = mean;
}
}
}
}
return bench_results;
}
async function main() {
await init();
await init_panic_hook();
return Comlink.proxy({
compactPublicKeyZeroKnowledgeBench,
});
}
Comlink.expose({
demos: main(),
});