diff --git a/.vscode/settings.json b/.vscode/settings.json index 8202b72..8cc0758 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -47,6 +47,8 @@ "optional": "cpp", "string_view": "cpp", "system_error": "cpp", - "initializer_list": "cpp" + "initializer_list": "cpp", + "__locale": "cpp", + "variant": "c" } -} \ No newline at end of file +} diff --git a/packages/epanet-engine/Dockerfile b/packages/epanet-engine/Dockerfile index df1ff7a..28d9236 100644 --- a/packages/epanet-engine/Dockerfile +++ b/packages/epanet-engine/Dockerfile @@ -1,4 +1,4 @@ -FROM trzeci/emscripten:1.39.4 +FROM emscripten/emsdk:3.1.15 RUN apt-get update && \ apt-get install -qqy git && \ diff --git a/packages/epanet-engine/build.sh b/packages/epanet-engine/build.sh index 21a1bb2..e5da89d 100755 --- a/packages/epanet-engine/build.sh +++ b/packages/epanet-engine/build.sh @@ -20,22 +20,30 @@ echo "=============================================" emcc -O3 -o ./build/epanetEngine.js /opt/epanet/build/lib/libepanet2.a \ -I /opt/epanet/src/include \ test.c \ - src/epanet_wrapper.cpp \ - --bind \ -s EXPORTED_FUNCTIONS="['_EN_geterror','_EN_getversion']" \ -s NO_EXIT_RUNTIME="1" \ -s DEAD_FUNCTIONS="[]" \ -s FORCE_FILESYSTEM="1" \ - -s INLINING_LIMIT="1" \ -s ALLOW_MEMORY_GROWTH="1" \ -s ERROR_ON_UNDEFINED_SYMBOLS=0 \ - -s EXPORTED_RUNTIME_METHODS='["ccall", "getValue", "UTF8ToString", "intArrayToString","FS"]' \ - -s WASM=0 \ - --llvm-lto 3 \ + -s 'EXPORTED_FUNCTIONS=["_free"]' \ + -s EXPORTED_RUNTIME_METHODS='["ccall", "getValue", "UTF8ToString", "stringToUTF8", "_free", "intArrayToString","FS"]' \ + -s WASM=1 \ + -s SINGLE_FILE=1 \ + -msimd128 \ + -s WASM_ASYNC_COMPILATION=0 \ --memory-init-file 0 \ --closure 0 #-s MODULARIZE=1 \ + #src/epanet_wrapper.cpp \ + #--bind \ + + # -msimd128 Enable SIMD https://jott.live/markdown/wasm_vector_addition + + #-s BINARYEN_ASYNC_COMPILATION=0 \ + #-s MODULARIZE=1 \ + cat src/wrapper/cjs-prefix.js build/epanetEngine.js src/wrapper/cjs-postfix.js >> index.js cat build/epanetEngine.js src/wrapper/es6-postfix.js >> index.es6.js diff --git a/packages/epanet-engine/epanet_version.c b/packages/epanet-engine/epanet_version.c new file mode 100644 index 0000000..b53ff5d --- /dev/null +++ b/packages/epanet-engine/epanet_version.c @@ -0,0 +1,10 @@ +#include "emscripten.h" +#include "epanet2_2.h" + +EMSCRIPTEN_KEEPALIVE +int getversion() +{ + int i; + EN_getversion(&i); + return i; +} \ No newline at end of file diff --git a/packages/epanet-engine/package.json b/packages/epanet-engine/package.json index f85250b..e000975 100644 --- a/packages/epanet-engine/package.json +++ b/packages/epanet-engine/package.json @@ -8,6 +8,7 @@ "scripts": { "build:dockerimage": "docker build -t mydockerimage .", "build:emscripten": "docker run --rm -v \"$(pwd):/src\" mydockerimage ./build.sh", + "build:emscripten_WASM": "docker run --rm -v \"$(pwd):/src\" mydockerimage ./wasm_build.sh", "build:app": "cp index.html dist/index.html", "build:typings": "cp src/index.d.ts dist/index.d.ts", "build": "yarn run build:emscripten && yarn run build:app && yarn run build:typings", diff --git a/packages/epanet-engine/src/epanet_wrapper.cpp b/packages/epanet-engine/src/epanet_wrapper.cpp index 049fd74..06dfdd5 100644 --- a/packages/epanet-engine/src/epanet_wrapper.cpp +++ b/packages/epanet-engine/src/epanet_wrapper.cpp @@ -32,6 +32,32 @@ class Epanet EN_deleteproject(ph); } + // temp speed tests + + int getnodeindex2(const std::string& id, intptr_t index) + { + int *ptr1 = reinterpret_cast(index); + //return EN_getnodeindex(ph, const_cast(id.c_str()), 0); + EN_Project ph2; + EN_createproject(&ph2); + + return EN_getnodeindex(ph2, "J1", ptr1); + } + + int getnodeindex(std::string id, intptr_t index) + { + int errcode; + int *ptr1 = reinterpret_cast(index); + char *idChar = new char[id.length() + 1]; + + strcpy(idChar, id.c_str()); + + errcode = EN_getnodeindex(ph, idChar, ptr1); + + delete[] idChar; + return errcode; + } + // Project Functions int open(std::string inputFile, std::string reportFile, std::string outputFile) @@ -408,19 +434,7 @@ class Epanet { return EN_deletenode(ph, index, actionCode); } - int getnodeindex(std::string id, intptr_t index) - { - int errcode; - int *ptr1 = reinterpret_cast(index); - char *idChar = new char[id.length() + 1]; - strcpy(idChar, id.c_str()); - - errcode = EN_getnodeindex(ph, idChar, ptr1); - - delete[] idChar; - return errcode; - } int getnodeid(int index, intptr_t out_id) { char *ptr1 = reinterpret_cast(out_id); @@ -995,6 +1009,7 @@ EMSCRIPTEN_BINDINGS(my_module) class_("Epanet") .constructor<>() + .function("getnodeindex2", &Epanet::getnodeindex2, allow_raw_pointers()) .function("open", &Epanet::open) .function("close", &Epanet::close) .function("runproject", &Epanet::runproject) diff --git a/packages/epanet-engine/src/index.d.ts b/packages/epanet-engine/src/index.d.ts index 0f2d5c7..1cc7baa 100644 --- a/packages/epanet-engine/src/index.d.ts +++ b/packages/epanet-engine/src/index.d.ts @@ -153,6 +153,8 @@ interface EpanetProjectConstructable { interface EpanetProject { // Generated methods + getnodeindex2(id: string, index: number): number; + //Project Functions open(inputFile: string, reportFile: string, outputFile: string): number; close(): number; diff --git a/packages/epanet-engine/test.c b/packages/epanet-engine/test.c index 3c0e5c5..dae4eb8 100644 --- a/packages/epanet-engine/test.c +++ b/packages/epanet-engine/test.c @@ -2,6 +2,63 @@ #include "epanet2_2.h" #include "stdio.h" +#include + + + + +EMSCRIPTEN_KEEPALIVE +EN_Project create_project() { + EN_Project ph = NULL; // EN_Project is already a pointer type. + int errcode = EN_createproject(&ph); + if (errcode != 0) { + // If there was an error, the memory (if allocated) should ideally be freed by the library. + // However, if you suspect a leak, you may need to handle it or check library docs. + return NULL; + } + return ph; +} + +EMSCRIPTEN_KEEPALIVE +int loadinp(EN_Project ph) { + int errcode = EN_open(ph, "test.inp", "test.rpt", "test.out"); + return errcode; +} + +EMSCRIPTEN_KEEPALIVE +int getNodeIndex(EN_Project ph, char *id) { + int index; + EN_getnodeindex(ph, id, &index); + return index; +} + +EMSCRIPTEN_KEEPALIVE +void free_project(EN_Project ph) { + free(ph); +} + + + + +//EMSCRIPTEN_KEEPALIVE +//int loadinp() +//{ +// int errcode; +// EN_createproject(&ph); +// errcode = EN_open(ph, "test.inp", "test.rpt", "test.out"); +// +// +// return errcode; +//} +// +//EMSCRIPTEN_KEEPALIVE +//int getNodeIndex() +//{ +// int index; +// EN_getnodeindex(ph, "J1", &index); +// return index; +//} + EMSCRIPTEN_KEEPALIVE int test() diff --git a/packages/epanet-engine/wasm_build.sh b/packages/epanet-engine/wasm_build.sh new file mode 100755 index 0000000..6043d20 --- /dev/null +++ b/packages/epanet-engine/wasm_build.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +set -e + + + +echo "=============================================" +echo "Compiling wasm " +echo "=============================================" +( + + mkdir -p build + + emcc epanet_version.c -o epanet_version.js \ + -I /opt/epanet/src/include \ + /opt/epanet/build/lib/libepanet2.a \ + -s WASM=1 -s "EXPORTED_FUNCTIONS=['_getversion']" + + mkdir -p dist + mv epanet_version.js dist + mv epanet_version.wasm dist + +) +echo "=============================================" +echo "Compiling wasm bindings done" +echo "=============================================" \ No newline at end of file diff --git a/packages/epanet-js/benchmark/engine.js b/packages/epanet-js/benchmark/engine.js new file mode 100644 index 0000000..bcf3bfa --- /dev/null +++ b/packages/epanet-js/benchmark/engine.js @@ -0,0 +1,88 @@ +const Benchmark = require('benchmark'); +const { epanetEngine } = require('../../epanet-engine/dist/index.js'); + +const _instance = epanetEngine; +const _FS = _instance.FS; + +_FS.writeFile( + 'test.inp', + `[TITLE] +Minimal EPANET Example + +[JUNCTIONS] +;ID Elev Demand Pattern + J1 100 10 ; + +[RESERVOIRS] +;ID Head Pattern + R1 150 ; + +[PIPES] +;ID Node1 Node2 Length Diameter Roughness MinorLoss Status + P1 R1 J1 1000 12 100 0 OPEN + +[COORDINATES] +;Node X-Coord Y-Coord + R1 1 1 + J1 2 1 + +[TAGS] + +[END]` +); + +//console.log(_instance); + +let projectPtr = _instance._create_project(); +let errorCode = _instance._loadinp(projectPtr); + +let nodeId = 'J1'; +let nodeIdPtr = _instance._malloc(nodeId.length + 1); // +1 for the null-terminator. +_instance.stringToUTF8(nodeId, nodeIdPtr, nodeId.length + 1); + +let index = _instance._getNodeIndex(projectPtr, nodeIdPtr); +console.log('Node index for', nodeId, 'is', index); + +const intPointer = _instance._malloc(4); +_instance._free(intPointer); +// +//_instance._getversion2(intPointer); +//const returnValue = _instance.getValue(intPointer, 'i32'); +// +//console.log(_instance._loadinp()); +//console.log(_instance._getNodeIndex()); + +let index2 = _instance.ccall( + 'getNodeIndex', // C function name + 'number', // Return type + ['number', 'string'], // Argument types + [projectPtr, nodeId] // Arguments +); + +console.log('Node index for', nodeId, 'is', index2); + +_instance.stringToUTF8(nodeId, nodeIdPtr, nodeId.length + 1); + +const suite = new Benchmark.Suite(); + +suite + .add('_getversion2', function() { + let index = _instance._getNodeIndex(projectPtr, nodeIdPtr); + }) + + .add('ccall', function() { + let index2 = _instance.ccall( + 'getNodeIndex', // C function name + 'number', // Return type + ['number', 'string'], // Argument types + [projectPtr, nodeId] // Arguments + ); + }) + + .on('cycle', function(event) { + console.log(String(event.target)); + }) + .on('complete', function() { + console.log('Fastest is ' + this.filter('fastest').map('name')); + }) + .run({ async: false }); diff --git a/packages/epanet-js/benchmark/index.js b/packages/epanet-js/benchmark/index.js new file mode 100644 index 0000000..e51c426 --- /dev/null +++ b/packages/epanet-js/benchmark/index.js @@ -0,0 +1,41 @@ +const Benchmark = require('benchmark'); +const { Project, Workspace } = require('../dist/index.js'); +const fs = require('fs'); + +const tankTestInp = fs.readFileSync( + __dirname + '/../test/data/tankTest.inp', + 'utf8' +); +const ws = new Workspace(); +ws.writeFile('tankTestInp.inp', tankTestInp); +const epanet = ws._instance.getversion(1); +console.log(epanet); +const model = new Project(ws); +model.open('tankTestInp.inp', 'tankTestInp.rpt', 'tankTestInp.bin'); + +//const suite = new Benchmark.Suite(); +// +//const intPointer = ws._instance._malloc(4); +// +//suite +// .add('getNodeIndex', function() { +// const junctionIndexLookup = model.getNodeIndex('J1'); +// //const version = ws.version; +// //ws._instance.getversion(intPointer); +// //const returnValue = ws._instance.getValue(intPointer, 'i32'); +// //ws.version; +// }) +// .add('getNodeIndex2', function() { +// //const junctionIndexLookup = model.getNodeIndex2('J1'); +// //const version = ws.version; +// //ws._instance.getversion(intPointer); +// //const returnValue = ws._instance.getValue(intPointer, 'i32'); +// //ws.version; +// }) +// .on('cycle', function(event) { +// console.log(String(event.target)); +// }) +// .on('complete', function() { +// console.log('Fastest is ' + this.filter('fastest').map('name')); +// }) +// .run({ async: false }); diff --git a/packages/epanet-js/benchmark/notes.md b/packages/epanet-js/benchmark/notes.md new file mode 100644 index 0000000..f324dda --- /dev/null +++ b/packages/epanet-js/benchmark/notes.md @@ -0,0 +1,102 @@ +Running a simple function like getNodeIndex with epanet-js can only run ~2.26M/sec + +The following was attempted to reduce overhead + +- Remove check error, no impact ~2.26M/sec +- remove spread on array for memory, ~2.76M/sec +- remove allocation of memory for each call, ~5.67M/sec +- remove slice with shared memory, ~6.6M/sec +- WASM 7.8M/sec + +Currently epanet js is wrapped with in C++ and uses the bind function with emscripten +If we expose the C functions directly we can get this between 20-35M/sec + +I tested using ccall comapred to manually allocating memory + + let index2 = _instance.ccall( + 'getNodeIndex', // C function name + 'number', // Return type + ['number', 'string'], // Argument types + [projectPtr, nodeId] // Arguments + ); + + vs + +``` +_instance.stringToUTF8(nodeId, nodeIdPtr, nodeId.length + 1); +let index = _instance._getNodeIndex(projectPtr, nodeIdPtr); +_instance._free(intPointer); +``` + +ccall was 6.9M/sec +manually was 32M/sec + +I also looked at other ways to pass values around and avoid recasting in C++ +I couldn't figure out how to pass the ptr directly, it wanted me to also sign the function that it was using raw pointers +In the end I put static values in there and found it was still limited, will need to confirm numbers again + +int getnodeindex2(const std::string& id, intptr*t index) +{ +int \_ptr1 = reinterpret_cast(index); +//return EN_getnodeindex(ph, const_cast(id.c_str()), 0); +EN_Project ph2; +EN_createproject(&ph2); + + return EN_getnodeindex(ph2, "J1", ptr1); + +} + +int getnodeindex(std::string id, intptr*t index) +{ +int errcode; +int \_ptr1 = reinterpret_cast(index); +char \*idChar = new char[id.length() + 1]; + + strcpy(idChar, id.c_str()); + + errcode = EN_getnodeindex(ph, idChar, ptr1); + + delete[] idChar; + return errcode; + +} + +The code to load it directly in C is as followed below. +I was originally concerned with how to deal with the project struct but it was pretty easy in the end + +``` +EMSCRIPTEN_KEEPALIVE +EN_Project create_project() { + EN_Project ph = NULL; // EN_Project is already a pointer type. + int errcode = EN_createproject(&ph); + if (errcode != 0) { + // If there was an error, the memory (if allocated) should ideally be freed by the library. + // However, if you suspect a leak, you may need to handle it or check library docs. + return NULL; + } + return ph; +} + +EMSCRIPTEN_KEEPALIVE +int loadinp(EN_Project ph) { + int errcode = EN_open(ph, "test.inp", "test.rpt", "test.out"); + return errcode; +} + +EMSCRIPTEN_KEEPALIVE +int getNodeIndex(EN_Project ph, char *id) { + int index; + EN_getnodeindex(ph, id, &index); + return index; +} + +EMSCRIPTEN_KEEPALIVE +void free_project(EN_Project ph) { + free(ph); +} +``` + +Some of my thinking is here: +https://chat.openai.com/c/514f602a-a47f-45d5-8c52-9fc178110327 + +Some other things I want to consider are how can easily export all the epanet functions without listing them diff --git a/packages/epanet-js/benchmark/test.inp b/packages/epanet-js/benchmark/test.inp new file mode 100644 index 0000000..169d0a1 --- /dev/null +++ b/packages/epanet-js/benchmark/test.inp @@ -0,0 +1,23 @@ +[TITLE] +Minimal EPANET Example + +[JUNCTIONS] +;ID Elev Demand Pattern + J1 100 10 ; + +[RESERVOIRS] +;ID Head Pattern + R1 150 ; + +[PIPES] +;ID Node1 Node2 Length Diameter Roughness MinorLoss Status + P1 R1 J1 1000 12 100 0 OPEN + +[COORDINATES] +;Node X-Coord Y-Coord + R1 1 1 + J1 2 1 + +[TAGS] + +[END] \ No newline at end of file diff --git a/packages/epanet-js/package.json b/packages/epanet-js/package.json index fe2e477..4f59153 100644 --- a/packages/epanet-js/package.json +++ b/packages/epanet-js/package.json @@ -19,7 +19,8 @@ "prepare": "tsdx build" }, "dependencies": { - "@model-create/epanet-engine": "0.7.0" + "@model-create/epanet-engine": "0.7.0", + "benchmark": "^2.1.4" }, "peerDependencies": {}, "husky": { diff --git a/packages/epanet-js/src/Project/Project.ts b/packages/epanet-js/src/Project/Project.ts index df32d8f..4926f3a 100644 --- a/packages/epanet-js/src/Project/Project.ts +++ b/packages/epanet-js/src/Project/Project.ts @@ -39,10 +39,16 @@ class Project _instance: EmscriptenModule; _EN: EpanetProject; + _sharedMemory: number[]; constructor(ws: Workspace) { this._ws = ws; this._instance = ws._instance; this._EN = new this._ws._instance.Epanet(); + this._sharedMemory = Array(7) + .fill(0) + .map(() => { + return this._instance._malloc(80); + }); } _getValue( @@ -57,7 +63,7 @@ class Project const size = type === 'int' ? 'i32' : type === 'long' ? 'i64' : 'double'; value = this._instance.getValue(pointer, size); } - this._instance._free(pointer); + //this._instance._free(pointer); return value; } @@ -97,6 +103,9 @@ class Project if (typeof v1 != 'string') { throw new Error('Method _allocateMemory expected string'); } + //return this._sharedMemory.slice(0, arguments.length); + return this._sharedMemory; + const types = Array.prototype.slice.call(arguments); return types.reduce((acc, t) => { let memsize; @@ -145,6 +154,8 @@ class Project // Implementing function classes + getNodeIndex2 = NetworkNodeFunctions.prototype.getNodeIndex2; + // Project Functions open = ProjectFunctions.prototype.open; close = ProjectFunctions.prototype.close; diff --git a/packages/epanet-js/src/Project/functions/NetworkNode.ts b/packages/epanet-js/src/Project/functions/NetworkNode.ts index e4654f1..0211579 100644 --- a/packages/epanet-js/src/Project/functions/NetworkNode.ts +++ b/packages/epanet-js/src/Project/functions/NetworkNode.ts @@ -12,9 +12,15 @@ class NetworkNodeFunctions { this._checkError(this._EN.deletenode(index, actionCode)); } + getNodeIndex2(this: Project, id: string) { + const memory = this._allocateMemory('int'); + this._EN.getnodeindex2(id, memory[0]); + return this._getValue(memory[0], 'int'); + } + getNodeIndex(this: Project, id: string) { const memory = this._allocateMemory('int'); - this._checkError(this._EN.getnodeindex(id, ...memory)); + this._EN.getnodeindex(id, memory[0]); return this._getValue(memory[0], 'int'); } diff --git a/yarn.lock b/yarn.lock index 13f168f..06914e9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1510,6 +1510,14 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +benchmark@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" + integrity sha512-l9MlfN4M1K/H2fbhfMy3B7vJd6AGKJVQn2h6Sg/Yx+KckoUA7ewS5Vv6TjSq18ooE1kS9hhAlQRH3AkXIh/aOQ== + dependencies: + lodash "^4.17.4" + platform "^1.3.3" + binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -4602,6 +4610,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +platform@^1.3.3: + version "1.3.6" + resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.6.tgz#48b4ce983164b209c2d45a107adb31f473a6e7a7" + integrity sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg== + please-upgrade-node@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz#aeddd3f994c933e4ad98b99d9a556efa0e2fe942"