Skip to content

Commit b4d4c92

Browse files
committed
Add wasm overhead benchmarks
1 parent 633403c commit b4d4c92

7 files changed

Lines changed: 397 additions & 0 deletions

File tree

SESSION.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,3 +862,11 @@
862862
- Decision: Added explicit WASM contract coverage for reachable negative-cycle rejection in Bellman-Ford wrappers.
863863
- Verified: `bash wasm/scripts/run_wasm_node_contract_tests.sh`; `git diff --check`.
864864
- Next: Commit, push, and close issue `#157`.
865+
866+
### 2026-05-10 - Issue #80
867+
868+
- Branch/commit:
869+
- Files: `wasm/benchmarks/native_overhead_bench.cpp`, `wasm/benchmarks/node_overhead_bench.mjs`, `wasm/scripts/run_overhead_benchmarks.sh`, `wasm/package.json`, `wasm/README.md`, `wasm/WASM.md`.
870+
- Decision: Added a local experimental overhead benchmark path comparing native C++, raw WASM runtime calls, and TypeScript facade calls without storing local results as release claims.
871+
- Verified: `npm --prefix wasm run build:types`; small `npm --prefix wasm run bench:overhead` smoke; `git diff --check`.
872+
- Next: Commit, push, and close issue `#80`.

wasm/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,37 @@ This command builds the wasm module (unless skipped), packs the current
9595
package, installs that tarball in an isolated fixture consumer, and runs a
9696
smoke test through the published package entrypoint.
9797

98+
## Local overhead benchmarks
99+
100+
The repository includes a small local benchmark suite for comparing native C++,
101+
raw WASM runtime calls, and the TypeScript facade on the same machine:
102+
103+
```bash
104+
cd wasm
105+
npm run bench:overhead
106+
```
107+
108+
The runner rebuilds the Node-compatible WASM module, builds a native C++
109+
benchmark binary, and prints CSV rows for:
110+
111+
- graph construction
112+
- BFS from one source
113+
- Dijkstra from one source
114+
- Floyd-Warshall on a small graph
115+
- attribute round-trips
116+
- multigraph edge-ID operations
117+
118+
You can tune the local smoke size with environment variables:
119+
120+
```bash
121+
NXPP_WASM_BENCH_ITERATIONS=10 NXPP_WASM_BENCH_NODES=500 npm run bench:overhead
122+
```
123+
124+
These numbers are local diagnostics, not release claims. Small repeated
125+
operations can be dominated by JS/WASM boundary cost, while larger algorithmic
126+
calls may amortize that overhead. Publish benchmark results only with the
127+
exact command, machine context, package version, and reproducible inputs.
128+
98129
## Publish order (npm first)
99130

100131
Use the package scripts to publish in a deterministic order:

wasm/WASM.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,18 @@ Run Node API contract tests:
104104
bash wasm/scripts/run_wasm_node_contract_tests.sh
105105
```
106106

107+
Run local native-vs-WASM overhead benchmarks:
108+
109+
```bash
110+
bash wasm/scripts/run_overhead_benchmarks.sh
111+
```
112+
113+
This benchmark path is intentionally local and experimental. It compares native
114+
C++, raw WASM runtime calls, and the TypeScript facade for representative graph
115+
construction, traversal, shortest-path, all-pairs, attribute, and multigraph
116+
edge-ID workloads. Treat its output as reproducible diagnostics only; do not use
117+
ad hoc local numbers as marketing or release claims.
118+
107119
## CI behavior
108120

109121
The experimental wasm workflow runs:
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#include <chrono>
2+
#include <cstddef>
3+
#include <cstdlib>
4+
#include <functional>
5+
#include <iomanip>
6+
#include <iostream>
7+
#include <string>
8+
#include <vector>
9+
10+
#include "include/nxpp.hpp"
11+
12+
namespace {
13+
14+
struct Config {
15+
int nodes = 300;
16+
int edges = 900;
17+
int floyd_nodes = 35;
18+
int attr_ops = 1000;
19+
int multigraph_edges = 600;
20+
int iterations = 5;
21+
};
22+
23+
template <typename T>
24+
void consume(const T& value) {
25+
#if defined(__GNUC__) || defined(__clang__)
26+
asm volatile("" : : "g"(&value) : "memory");
27+
#else
28+
(void)value;
29+
#endif
30+
}
31+
32+
int read_int_arg(int argc, char** argv, const std::string& name, int fallback) {
33+
for (int i = 1; i + 1 < argc; ++i) {
34+
if (argv[i] == name) {
35+
return std::atoi(argv[i + 1]);
36+
}
37+
}
38+
return fallback;
39+
}
40+
41+
Config read_config(int argc, char** argv) {
42+
Config config;
43+
config.nodes = read_int_arg(argc, argv, "--nodes", config.nodes);
44+
config.edges = read_int_arg(argc, argv, "--edges", config.edges);
45+
config.floyd_nodes = read_int_arg(argc, argv, "--floyd-nodes", config.floyd_nodes);
46+
config.attr_ops = read_int_arg(argc, argv, "--attr-ops", config.attr_ops);
47+
config.multigraph_edges = read_int_arg(argc, argv, "--multigraph-edges", config.multigraph_edges);
48+
config.iterations = read_int_arg(argc, argv, "--iterations", config.iterations);
49+
return config;
50+
}
51+
52+
nxpp::DiGraphInt make_digraph(int nodes, int edges) {
53+
nxpp::DiGraphInt graph;
54+
for (int node = 0; node < nodes; ++node) {
55+
graph.add_node(node);
56+
}
57+
for (int edge = 0; edge < edges; ++edge) {
58+
const int source = edge % nodes;
59+
int target = (edge * 37 + 11) % nodes;
60+
if (target == source) {
61+
target = (target + 1) % nodes;
62+
}
63+
graph.add_edge(source, target, static_cast<double>((edge % 17) + 1));
64+
}
65+
return graph;
66+
}
67+
68+
nxpp::DiGraphInt make_floyd_graph(int nodes) {
69+
nxpp::DiGraphInt graph;
70+
for (int node = 0; node < nodes; ++node) {
71+
graph.add_node(node);
72+
}
73+
for (int node = 0; node + 1 < nodes; ++node) {
74+
graph.add_edge(node, node + 1, 1.0);
75+
if (node + 3 < nodes) {
76+
graph.add_edge(node, node + 3, 4.0);
77+
}
78+
}
79+
return graph;
80+
}
81+
82+
template <typename Fn>
83+
double measure_ms(int iterations, Fn&& fn) {
84+
const auto start = std::chrono::steady_clock::now();
85+
for (int i = 0; i < iterations; ++i) {
86+
fn();
87+
}
88+
const auto stop = std::chrono::steady_clock::now();
89+
return std::chrono::duration<double, std::milli>(stop - start).count();
90+
}
91+
92+
void print_row(const std::string& workload, int iterations, double total_ms, const std::string& notes) {
93+
const double ops_per_second = total_ms > 0.0 ? (static_cast<double>(iterations) * 1000.0 / total_ms) : 0.0;
94+
std::cout << "native_cpp," << workload << "," << iterations << ","
95+
<< std::fixed << std::setprecision(3) << total_ms << ","
96+
<< std::fixed << std::setprecision(3) << ops_per_second << ","
97+
<< notes << "\n";
98+
}
99+
100+
} // namespace
101+
102+
int main(int argc, char** argv) {
103+
const Config config = read_config(argc, argv);
104+
std::cout << "layer,workload,iterations,total_ms,ops_per_second,notes\n";
105+
106+
const double construction_ms = measure_ms(config.iterations, [&] {
107+
auto graph = make_digraph(config.nodes, config.edges);
108+
consume(graph.num_edges());
109+
});
110+
print_row("construct_digraph", config.iterations, construction_ms, "nodes=" + std::to_string(config.nodes) + ";edges=" + std::to_string(config.edges));
111+
112+
auto graph = make_digraph(config.nodes, config.edges);
113+
const double bfs_ms = measure_ms(config.iterations, [&] {
114+
const auto edges = graph.bfs_edges(0);
115+
consume(edges.size());
116+
});
117+
print_row("bfs_edges", config.iterations, bfs_ms, "source=0");
118+
119+
const double dijkstra_ms = measure_ms(config.iterations, [&] {
120+
const auto result = graph.dijkstra_shortest_paths(0);
121+
consume(result.distance.size());
122+
});
123+
print_row("dijkstra_shortest_paths", config.iterations, dijkstra_ms, "source=0");
124+
125+
auto floyd_graph = make_floyd_graph(config.floyd_nodes);
126+
const double floyd_ms = measure_ms(config.iterations, [&] {
127+
const auto matrix = floyd_graph.floyd_warshall_all_pairs_shortest_paths();
128+
consume(matrix.size());
129+
});
130+
print_row("floyd_warshall_all_pairs", config.iterations, floyd_ms, "nodes=" + std::to_string(config.floyd_nodes));
131+
132+
nxpp::DiGraphInt attr_graph;
133+
const double attr_ms = measure_ms(config.iterations, [&] {
134+
for (int i = 0; i < config.attr_ops; ++i) {
135+
const int node = i % config.nodes;
136+
attr_graph.node(node)["label"] = std::string("node-") + std::to_string(node);
137+
const auto value = attr_graph.get_node_attr<std::string>(node, "label");
138+
consume(value);
139+
}
140+
});
141+
print_row("attribute_roundtrip", config.iterations, attr_ms, "ops_per_iteration=" + std::to_string(config.attr_ops));
142+
143+
const double multigraph_ms = measure_ms(config.iterations, [&] {
144+
nxpp::MultiDiGraphInt multigraph;
145+
for (int i = 0; i < config.multigraph_edges; ++i) {
146+
const auto id = multigraph.add_edge_with_id(i % config.nodes, (i + 1) % config.nodes, 1.0);
147+
multigraph.set_edge_attr(id, "capacity", i + 1);
148+
}
149+
const auto ids = multigraph.edge_ids(0, 1);
150+
consume(ids.size());
151+
});
152+
print_row("multigraph_edge_ids", config.iterations, multigraph_ms, "edges=" + std::to_string(config.multigraph_edges));
153+
154+
return 0;
155+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { performance } from "node:perf_hooks";
2+
3+
import nxpp from "../dist/index.js";
4+
5+
function readIntArg(name, fallback) {
6+
const index = process.argv.indexOf(name);
7+
if (index === -1 || index + 1 >= process.argv.length) {
8+
return fallback;
9+
}
10+
const value = Number.parseInt(process.argv[index + 1], 10);
11+
return Number.isFinite(value) ? value : fallback;
12+
}
13+
14+
const config = {
15+
nodes: readIntArg("--nodes", 300),
16+
edges: readIntArg("--edges", 900),
17+
floydNodes: readIntArg("--floyd-nodes", 35),
18+
attrOps: readIntArg("--attr-ops", 1000),
19+
multigraphEdges: readIntArg("--multigraph-edges", 600),
20+
iterations: readIntArg("--iterations", 5),
21+
};
22+
23+
function consume(value) {
24+
globalThis.__nxppBenchmarkSink = value;
25+
}
26+
27+
function toArray(value) {
28+
return Array.from(value);
29+
}
30+
31+
function measureMs(iterations, fn) {
32+
const start = performance.now();
33+
for (let i = 0; i < iterations; i += 1) {
34+
fn();
35+
}
36+
return performance.now() - start;
37+
}
38+
39+
function printHeader() {
40+
console.log("layer,workload,iterations,total_ms,ops_per_second,notes");
41+
}
42+
43+
function printRow(layer, workload, iterations, totalMs, notes) {
44+
const opsPerSecond = totalMs > 0 ? (iterations * 1000) / totalMs : 0;
45+
console.log([
46+
layer,
47+
workload,
48+
iterations,
49+
totalMs.toFixed(3),
50+
opsPerSecond.toFixed(3),
51+
notes,
52+
].join(","));
53+
}
54+
55+
function makeDiGraph(GraphCtor, nodes = config.nodes, edges = config.edges) {
56+
const graph = new GraphCtor();
57+
for (let node = 0; node < nodes; node += 1) {
58+
graph.addNode(node);
59+
}
60+
for (let edge = 0; edge < edges; edge += 1) {
61+
const source = edge % nodes;
62+
let target = (edge * 37 + 11) % nodes;
63+
if (target === source) {
64+
target = (target + 1) % nodes;
65+
}
66+
graph.addEdge(source, target, (edge % 17) + 1);
67+
}
68+
return graph;
69+
}
70+
71+
function makeFloydGraph(GraphCtor) {
72+
const graph = new GraphCtor();
73+
for (let node = 0; node < config.floydNodes; node += 1) {
74+
graph.addNode(node);
75+
}
76+
for (let node = 0; node + 1 < config.floydNodes; node += 1) {
77+
graph.addEdge(node, node + 1, 1);
78+
if (node + 3 < config.floydNodes) {
79+
graph.addEdge(node, node + 3, 4);
80+
}
81+
}
82+
return graph;
83+
}
84+
85+
function dispose(graph) {
86+
if (typeof graph.dispose === "function") {
87+
graph.dispose();
88+
return;
89+
}
90+
if (typeof graph.delete === "function") {
91+
graph.delete();
92+
}
93+
}
94+
95+
function runLayer(layer, constructors) {
96+
const constructionMs = measureMs(config.iterations, () => {
97+
const graph = makeDiGraph(constructors.DiGraphInt);
98+
consume(toArray(graph.nodes()).length);
99+
dispose(graph);
100+
});
101+
printRow(layer, "construct_digraph", config.iterations, constructionMs, `nodes=${config.nodes};edges=${config.edges}`);
102+
103+
const graph = makeDiGraph(constructors.DiGraphInt);
104+
const bfsMs = measureMs(config.iterations, () => {
105+
consume(toArray(graph.bfsEdges(0)).length);
106+
});
107+
printRow(layer, "bfs_edges", config.iterations, bfsMs, "source=0");
108+
109+
const dijkstraMs = measureMs(config.iterations, () => {
110+
const result = graph.dijkstraShortestPaths(0);
111+
consume(toArray(result.distance).length);
112+
});
113+
printRow(layer, "dijkstra_shortest_paths", config.iterations, dijkstraMs, "source=0");
114+
dispose(graph);
115+
116+
const floydGraph = makeFloydGraph(constructors.DiGraphInt);
117+
const floydMs = measureMs(config.iterations, () => {
118+
consume(toArray(floydGraph.floydWarshallAllPairsShortestPaths()).length);
119+
});
120+
printRow(layer, "floyd_warshall_all_pairs", config.iterations, floydMs, `nodes=${config.floydNodes}`);
121+
dispose(floydGraph);
122+
123+
const attrGraph = new constructors.DiGraphInt();
124+
const attrMs = measureMs(config.iterations, () => {
125+
for (let i = 0; i < config.attrOps; i += 1) {
126+
const node = i % config.nodes;
127+
attrGraph.setNodeAttr(node, "label", `node-${node}`);
128+
consume(attrGraph.getNodeAttr(node, "label"));
129+
}
130+
});
131+
printRow(layer, "attribute_roundtrip", config.iterations, attrMs, `ops_per_iteration=${config.attrOps}`);
132+
dispose(attrGraph);
133+
134+
const multigraphMs = measureMs(config.iterations, () => {
135+
const multigraph = new constructors.MultiDiGraphInt();
136+
for (let i = 0; i < config.multigraphEdges; i += 1) {
137+
multigraph.addEdge(i % config.nodes, (i + 1) % config.nodes, 1);
138+
}
139+
const ids = toArray(multigraph.edgeIdsBetween(0, 1));
140+
for (const id of ids) {
141+
multigraph.setEdgeAttrById(id, "capacity", id + 1);
142+
}
143+
consume(ids.length);
144+
dispose(multigraph);
145+
});
146+
printRow(layer, "multigraph_edge_ids", config.iterations, multigraphMs, `edges=${config.multigraphEdges}`);
147+
}
148+
149+
printHeader();
150+
const rawRuntime = await nxpp.createNxpp();
151+
runLayer("raw_wasm", rawRuntime);
152+
runLayer("facade_ts", nxpp);

wasm/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
},
3535
"scripts": {
3636
"build:types": "tsc -p tsconfig.json",
37+
"bench:overhead": "bash scripts/run_overhead_benchmarks.sh",
3738
"check:node-contract": "bash scripts/run_wasm_node_contract_tests.sh",
3839
"check:npm-pack-consumer": "bash scripts/run_npm_pack_consumer_test.sh",
3940
"publish:npm": "npm publish --registry=https://registry.npmjs.org/",

0 commit comments

Comments
 (0)