From dbffe5652a263dfcc55611a7d9dfbb1f779fe48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Sj=C3=B6lund?= Date: Tue, 25 Aug 2026 19:15:43 +0200 Subject: [PATCH] Simulate one wasm artifact three ways `--wasmjitrunner=sim,me,cs` exports every model once as a wasm artifact and simulates that one artifact each of those ways, so a model is translated and compiled once however many runs it gets: | runner | branch | what runs | | --- | --- | --- | | `sim` | `` | the simulation runtime inside the artifact | | `me` | `-me` | FMI 3.0 Model Exchange, omc integrating | | `cs` | `-cs` | FMI 3.0 Co-Simulation, the artifact integrating | ```bash ./test.py --branch=master-wasm-jit --wasmjitrunner=sim,me,cs \ --extraflags='--simCodeTarget=wasm-jit' configs/...json ./report.py --branches="master-wasm-jit master-wasm-jit-me master-wasm-jit-cs" ``` It reuses the fan-out `--fmisimulator` already had -- one build, a result branch per runner, the build phase shared and the simulation and verification per runner -- so `runnerNames` in test.py and `runners` in testmodel.py stand for whichever of the two a job asked for. The two are mutually exclusive. The runners live in `configs/wasm-jit-runners.json`, so adding a fourth is an entry there: `simflags` is what is appended to the model's simulation flags and `branchSuffix` overrides the `-` it adds to the branch. The build is `buildModelFMU(..., fmuType="me_cs", version="3.0", platforms={"wasm"})` with `--fmuDirectory=true`, which writes the model kernel unzipped; omc links it against an adapter it compiled once into its cache, so nothing is packed, extracted or loaded per run. Each run reports what that cost in its `.sim`. ## Jenkins The `wasm_jit` job runs the three-way target: `--nobuildmodel` becomes `--wasmjitrunner=sim,me,cs`, and `GITBRANCHES_WASM_JIT` names the three branches it fills, so `all-reports.py` and `all-plots.py` cover them without further change. `overview-wasm-jit.html` was `wasm-jit` against master and is now the three faces against it, which is where they are worth comparing: they share an export, so only the simulation and the verification tell them apart. Needs an omc with the wasm artifact support (OpenModelica feature/wasm-jit-artifact). Assisted-by: Claude Opus 5 (1M context) --- .CI/Jenkinsfile | 10 ++-- README.md | 46 ++++++++++++++++ configs/wasm-jit-runners.json | 29 ++++++++++ shared.py | 40 ++++++++++++++ test.py | 23 ++++++-- testmodel.py | 101 ++++++++++++++++++++++++++++------ testresults.md | 6 ++ 7 files changed, 228 insertions(+), 27 deletions(-) create mode 100644 configs/wasm-jit-runners.json diff --git a/.CI/Jenkinsfile b/.CI/Jenkinsfile index 565d20d..c7d2092 100644 --- a/.CI/Jenkinsfile +++ b/.CI/Jenkinsfile @@ -35,7 +35,7 @@ pipeline { booleanParam(name: 'gbode', defaultValue: false, description: 'master branch, with -d=newInst and -s gbode (ryzen-5950x-2). This is an experimental job that does not run on a fixed schedule.') booleanParam(name: 'ida', defaultValue: false, description: 'master branch, with -d=newInst and -s ida (ryzen-5950x-2). This is an experimental job that does not run on a fixed schedule.') booleanParam(name: 'generateSymbolicJacobian', defaultValue: false, description: 'master branch, with --generateSymbolicJacobian (ryzen-5950x-1). This is an experimental job that does not run on a fixed schedule.') - booleanParam(name: 'wasm_jit', defaultValue: false, description: 'master branch, with --simCodeTarget=wasm-jit (ryzen-5950x-2). This is an experimental job that does not run on a fixed schedule.') + booleanParam(name: 'wasm_jit', defaultValue: false, description: 'master branch, with --simCodeTarget=wasm-jit (ryzen-9950x). Every model is exported once as a wasm artifact and simulated three ways, filling wasm-jit (the runtime inside the artifact), wasm-jit-me (FMI 3.0 Model Exchange) and wasm-jit-cs (FMI 3.0 Co-Simulation). This is an experimental job that does not run on a fixed schedule.') booleanParam(name: 'heavy_tests', defaultValue: false, description: 'master branch, runs one test at a time. That is, no parallel launching of tests. omc will use multiple threads for each test (-n=1 is not set unlike the other regression tests.), (ryzen-5950x-1). This is an experimental job that does not run on a fixed schedule.') string(name: 'pull_request', defaultValue: '', description: 'Test an OpenModelica pull request rather than a branch: its number, e.g. 16354. omc is built from refs/pull//merge - the pull request as it would land - the results fill a pr- table, and the report compares them against the newest run of pull_request_baseline. Left empty, nothing of this runs.') @@ -384,7 +384,7 @@ pipeline { expression { params.wasm_jit } } steps { - runRegressiontest('master', 'wasm-jit', 'setCommandLineOptions("--simCodeTarget=wasm-jit")', '', false, '', '--nobuildmodel', false, false, 0, 'configs/conf.json', + runRegressiontest('master', 'wasm-jit', 'setCommandLineOptions("--simCodeTarget=wasm-jit")', '', false, '', '--wasmjitrunner=sim,me,cs', false, false, 0, 'configs/conf.json', '-DOM_OMC_ENABLE_RUST=ON -DRUST_OMC_CI=ON -DRUST_OMC_THREADS=4 -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache', '.CI/wasm-jit') } @@ -492,7 +492,7 @@ pipeline { GITBRANCHES_DAE = 'newInst-daeMode' GITBRANCHES_NEWBACKEND_DAE = 'newBackend-daeMode' GITBRANCHES_CPP = 'v1.19-cpp v1.20-cpp v1.21-cpp v1.22-cpp v1.23-cpp v1.24-cpp v1.25-cpp cpp v1.26-cpp v1.27-cpp' - GITBRANCHES_WASM_JIT = 'wasm-jit' + GITBRANCHES_WASM_JIT = 'wasm-jit wasm-jit-me wasm-jit-cs' GITBRANCHES_SPECIAL = 'master wasm-jit newInst-newBackend' PYTHONIOENCODING = 'utf-8' IDA_EMAIL = credentials('IDA email') @@ -576,7 +576,9 @@ pipeline { sh "./report.py --branches='ida master' configs/conf.json" sh "mv overview.html overview-ida.html" - sh "./report.py --branches='wasm-jit master' configs/conf.json" + // The three ways one wasm artifact is simulated, against master: they share + // an export, so only the simulation and the verification tell them apart. + sh "./report.py --branches='${env.GITBRANCHES_WASM_JIT} master' configs/conf.json" sh "mv overview.html overview-wasm-jit.html" sh "./report.py --branches='${env.GITBRANCHES_CPP}' configs/conf.json" diff --git a/README.md b/README.md index cc998b3..c919d7a 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,10 @@ Options: simulate every FMU with several tools without building it more than once, see [Testing FMI with several simulators](#testing-fmi-with-several-simulators) +- `--wasmjitrunner=[]`: Export every model once as a wasm artifact and simulate + that one artifact several ways, see [One wasm artifact, + three ways to simulate + it](#one-wasm-artifact-three-ways-to-simulate-it) - `--ulimitvmem=8388608`: Virtual memory limit (in kB) - `--default=[]`: Add a default value for some configuration key, such as `--default=ulimitExe=60`. The equals sign is mandatory @@ -255,6 +259,48 @@ A tool that is a Python package rather than a command line needs a small driver script that takes the arguments its entry passes, simulates, writes the result file and exits non-zero when it fails; the entry then points `command` at it. +### One wasm artifact, three ways to simulate it + +`--simCodeTarget=wasm-jit` can export a model as a single WebAssembly artifact +that carries three things at once: the model's own simulation runtime, an FMI +3.0 Model Exchange interface and an FMI 3.0 Co-Simulation interface. Exporting +it costs one translation and one compilation; simulating it three ways then +costs three simulations and nothing else, which is the same bargain the FMI +simulators above strike. + +```bash +./test.py --branch=master-wasm-jit --wasmjitrunner=sim,me,cs \ + --extraflags='--simCodeTarget=wasm-jit' configs/myConf.json +./report.py --branches="master-wasm-jit master-wasm-jit-me master-wasm-jit-cs" +# the overview.html it writes is published as overview-wasm-jit.html +``` + +The build phase is `buildModelFMU(..., fmuType="me_cs", version="3.0", +platforms={"wasm"})` with `--fmuDirectory=true`, which writes `.fmu` as a +directory holding the model description and the model kernel. Each runner then +simulates it through `simulate(..., resimulateExecutable=".fmu")`, omc +linking that kernel against an FMI 3.0 adapter it compiled once into +`~/.openmodelica/cache` — so a model is translated once, compiled once, and +neither packed nor loaded: + +| runner | branch | what runs | +| --- | --- | --- | +| `sim` | `master-wasm-jit` | the simulation runtime inside the artifact, in wasm | +| `me` | `master-wasm-jit-me` | FMI 3.0 Model Exchange, integrated by omc with DASKR | +| `cs` | `master-wasm-jit-cs` | FMI 3.0 Co-Simulation, the artifact integrating itself with DASKR | + +Every run reports what loading and linking the artifact cost, so the `.sim` +files say how much of a short simulation is the artifact and how much is the +model. + +The runners live in +[configs/wasm-jit-runners.json](configs/wasm-jit-runners.json); adding one is an +entry there (`simflags` is what is appended to the model's simulation flags, +`branchSuffix` overrides the `-` it adds to the branch). + +`--wasmjitrunner` and `--fmisimulator` both fan one build out into several +result branches, so a job uses one of them, not both. + ### Testing a pull request A branch is tested against its own previous run, which says what broke *after* a diff --git a/configs/wasm-jit-runners.json b/configs/wasm-jit-runners.json new file mode 100644 index 0000000..42496f0 --- /dev/null +++ b/configs/wasm-jit-runners.json @@ -0,0 +1,29 @@ +{ + "_comment": [ + "The ways one exported wasm artifact can be simulated. Adding one is an entry here and nothing else.", + "", + "A wasm artifact is what buildModelFMU(..., fmuType=\"me_cs\", platforms={\"wasm\", })", + "writes: one .wasm component carrying the model kernel, the FMI 3.0 adapter and the model's own", + "simulation runtime, plus that component compiled ahead of time to a .cwasm. Every runner below", + "simulates that one artifact through simulate(..., resimulateExecutable=\".fmu\"), so the", + "model is translated and compiled once however many of them run.", + "", + "simflags what is appended to the model's simulation flags; -s fmi3:… picks the interface.", + "description what the runner is, for the reports.", + "branchSuffix appended to --branch to get the table of this runner; the default is -, so", + " the artifact's own simulation runtime is the one that needs to say anything here." + ], + "sim": { + "branchSuffix": "", + "simflags": "", + "description": "the artifact's own simulation runtime, run inside the wasm module" + }, + "me": { + "simflags": "-s fmi3:me:daskr", + "description": "FMI 3.0 Model Exchange, integrated by omc with DASKR" + }, + "cs": { + "simflags": "-s fmi3:cs", + "description": "FMI 3.0 Co-Simulation, the artifact integrating itself with DASKR" + } +} diff --git a/shared.py b/shared.py index 89f6a8a..eed9dc5 100644 --- a/shared.py +++ b/shared.py @@ -214,6 +214,46 @@ def fmiSimulatorCommand(name, command, **values): return "%s %s" % (spec.get("command", "{simulator}").format(**values), spec["arguments"].format(**values)) +# The ways an exported wasm artifact can be simulated, in configs/wasm-jit-runners.json. +# The same shape as the FMI simulators above, except that a runner is not a tool +# to invoke: it is a set of simulation flags omc itself is given, since the +# artifact is run inside omc. +WASM_JIT_RUNNERS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "configs", "wasm-jit-runners.json") +_wasmJitRunners = None + +def wasmJitRunners(path=None): + """Everything the testing knows about the wasm-jit artifact runners.""" + global _wasmJitRunners + if _wasmJitRunners is None or path: + with open(path or WASM_JIT_RUNNERS_FILE) as fin: + _wasmJitRunners = dict((k, v) for (k, v) in json.load(fin).items() if not k.startswith("_")) + return _wasmJitRunners + +def wasmJitRunner(name): + known = wasmJitRunners() + if name not in known: + raise Exception("Unknown wasm-jit runner %s; known are %s. Adding one is an entry in %s." + % (name, ", ".join(sorted(known)), WASM_JIT_RUNNERS_FILE)) + return known[name] + +def parseWasmJitRunners(names): + """The --wasmjitrunner values as an ordered list of (name, simflags).""" + res = [] + for spec in names or []: + for name in spec.split(","): + name = name.strip() + if name: + res.append((name, wasmJitRunner(name).get("simflags") or "")) + seen = [n for (n, _) in res] + if len(set(seen)) != len(seen): + raise Exception("The same wasm-jit runner name is used twice: %s" % ", ".join(seen)) + return res + +def branchForWasmJitRunner(branch, name): + """Where the results of one wasm-jit runner are stored: --branch, then -.""" + return branch + wasmJitRunner(name).get("branchSuffix", "-%s" % name) + def branchForSimulator(branch, name): """Where the results of one FMI simulator of a run are stored. diff --git a/test.py b/test.py index ced5d64..7e4bca9 100755 --- a/test.py +++ b/test.py @@ -37,6 +37,7 @@ parser.add_argument('--nobuildmodel', action="store_true", help="Translate, build and simulate in a single simulate() call instead of translateModel() followed by simulate(resimulateExecutable=...), so the JIT compile is reported as build time rather than simulation time. Only used by simCodeTarget=wasm-jit.", default=False) parser.add_argument('--coldhot', action="store_true", help="Simulate each model twice in the same omc; the second run reuses the compiled module. Both times are printed, but only the hot one is stored. Only used by simCodeTarget=wasm-jit.", default=False) parser.add_argument('--fmisimulator', action='append', default=[], help="FMI simulator to run the FMUs with, as 'name=command' or just the command. Repeat it to simulate every FMU with several tools without building it more than once; the first one stores its results in --branch and each further one in -, so --branch=master-fmi with OMSimulator and fmpy fills master-fmi and master-fmi-fmpy." ) +parser.add_argument('--wasmjitrunner', action='append', default=[], help="Export every model once as a wasm artifact (buildModelFMU with fmuType=me_cs, platforms={wasm,}) and simulate that one artifact each of these ways: 'sim' runs the simulation runtime inside it, 'me' and 'cs' its FMI 3.0 interfaces. Comma-separated or repeated; the first fills --branch and each further one -, so --branch=master-wasm-jit with sim,me,cs fills master-wasm-jit, master-wasm-jit-me and master-wasm-jit-cs. See configs/wasm-jit-runners.json. Only for simCodeTarget=wasm-jit.") parser.add_argument('--ulimitvmem', help="Virtual memory limit (in kB) (linux only)", type=int, default=8*1024*1024) parser.add_argument('--default', action='append', help="Add a default value for some configuration key, such as --default=ulimitExe=60. The equals sign is mandatory.", default=[]) parser.add_argument('-j', '--jobs', default=0, help="Ignored and deprecated, use procOMC:0 or procOMC:1 in the config") @@ -71,16 +72,26 @@ fmisimulators = shared.parseFmiSimulators(args.fmisimulator) # The first simulator is the one the single-simulator code paths use. fmisimulator = fmisimulators[0][1] if fmisimulators else None +wasmjitrunners = shared.parseWasmJitRunners(args.wasmjitrunner) +if fmisimulators and wasmjitrunners: + raise Exception("--fmisimulator and --wasmjitrunner both fan one build out into several result " + "branches; a job runs one of them, not both.") +# Everything one build is simulated by, whichever of the two it is. +runnerNames = [n for (n, _) in fmisimulators or wasmjitrunners] + +def branchForRunner(name): + return (shared.branchForSimulator(branch, name) if fmisimulators + else shared.branchForWasmJitRunner(branch, name)) # One branch per simulator. The first reports itself in the results of the # model and the others under their own name, but every one of them stores its # results where its own simulator belongs: a job given only FMPy on # --branch=v1.27-fmi fills v1.27-fmi-fmpy, not v1.27-fmi. resultBranches = [(branch, None)] -if fmisimulators: - resultBranches = [(shared.branchForSimulator(branch, fmisimulators[0][0]), None)] - for (simulatorName, _) in fmisimulators[1:]: - resultBranches.append((shared.branchForSimulator(branch, simulatorName), simulatorName)) +if runnerNames: + resultBranches = [(branchForRunner(runnerNames[0]), None)] + for simulatorName in runnerNames[1:]: + resultBranches.append((branchForRunner(simulatorName), simulatorName)) # What the run asks about when it looks for results it already has. primaryBranch = resultBranches[0][0] @@ -646,6 +657,8 @@ def hashReferenceFiles(s): conf["fmisimulator"] = fmisimulator conf["fmisimulators"] = ["%s=%s" % (n, c) for (n, c) in fmisimulators] conf["fmuType"] = fmuType + if wasmjitrunners: + conf["wasmjitrunners"] = [n for (n, _) in wasmjitrunners] if (not canChangeOptLevel) and "optlevel" in conf: print("Deleting optlevel") del conf["optlevel"] @@ -1150,7 +1163,7 @@ def dataForSimulator(data, simulator): def artifactSuffix(simulator): """What tells the files of one simulator from those of another.""" - return "_%s" % simulator if simulator and len(fmisimulators) > 1 else "" + return "_%s" % simulator if simulator and len(runnerNames) > 1 else "" # Every simulator publishes its own results - .sim and diff files included - to # the directory of its own branch; the .err of the build is shared, so each of diff --git a/testmodel.py b/testmodel.py index ecc4af3..d236325 100755 --- a/testmodel.py +++ b/testmodel.py @@ -281,10 +281,14 @@ def target(res): isWasmJit = conf["simCodeTarget"]=="wasm-jit" # --nobuildmodel: one simulate() instead of translateModel()+resimulate, so omc # reports the build/simulation split itself -useSimulate = isWasmJit and conf.get("noBuildModel") and not conf.get("fmi") +# --wasmjitrunner: export the model once as a wasm artifact and simulate that one +# artifact several ways (its own simulation runtime, FMI 3.0 ME, FMI 3.0 CS), so +# the model is translated and compiled once however many runs there are +useArtifact = isWasmJit and bool(conf.get("wasmjitrunners")) and not conf.get("fmi") +useSimulate = isWasmJit and conf.get("noBuildModel") and not conf.get("fmi") and not useArtifact # --coldhot: simulate again in the same session, where the module is already # compiled, and report that run instead -useColdHot = isWasmJit and conf.get("coldHot") and not conf.get("fmi") +useColdHot = isWasmJit and conf.get("coldHot") and not conf.get("fmi") and not useArtifact if isWasmJit and conf.get("fmi"): with open(errFile, 'a+') as fp: fp.write("FMI export is not supported for simCodeTarget=wasm-jit") @@ -481,6 +485,13 @@ def simulateCmd(resimulate): if conf.get("fmi"): cmd='"" <> buildModelFMU(%s,fileNamePrefix="%s",fmuType="%s",version="%s",platforms={"static"})' % (conf["modelName"],conf["fileName"].replace(".","_"),conf["fmuType"],conf["fmi"]) timedPhase = "build" +elif useArtifact: + # One artifact for every runner, written unzipped: the model kernel alone, + # which omc links against an adapter it compiled once into its cache. Nothing + # here is packed, extracted or compiled but the model. + sendExpressionOldOrNew('setCommandLineOptions("--fmuDirectory=true")') + cmd='"" <> buildModelFMU(%s,fileNamePrefix="%s",fmuType="me_cs",version="3.0",platforms={"wasm"})' % (conf["modelName"],conf["fileName"].replace(".","_")) + timedPhase = "build" elif useSimulate: cmd=simulateCmd(resimulate=False) timeout = conf["ulimitOmc"] + conf["ulimitExe"] @@ -567,12 +578,12 @@ def simulateCmd(resimulate): start=monotonic() try: - if conf.get("fmi"): + if conf.get("fmi") or useArtifact: if res: fmuExpectedLocation = "%s.fmu" % conf["fileName"].replace(".","_") execstat["build"] = max(0.0, buildmodel) # Older versions didn't separate translate and build times if not os.path.exists(os.path.normpath(fmuExpectedLocation)): - err += "\nFMU was not generated in the expected location: %s" % fmuExpectedLocation + err += "\n%s was not generated in the expected location: %s" % ("The wasm artifact" if useArtifact else "FMU", fmuExpectedLocation) execstat["phase"]=4 writeResultAndExit(0, False, omc, omc_new) execstat["phase"] = 5 @@ -609,6 +620,10 @@ def simulateCmd(resimulate): fmisimulators = shared.parseFmiSimulators(conf.get("fmisimulators")) if conf.get("fmi") else [] if conf.get("fmi") and not fmisimulators and fmisimulator: fmisimulators = shared.parseFmiSimulators([fmisimulator]) +wasmjitrunners = shared.parseWasmJitRunners(conf.get("wasmjitrunners")) if useArtifact else [] +# One build, several runs: an FMU simulated by several tools and a wasm artifact +# run several ways fan out the same way, so they share the naming below. +runners = fmisimulators or wasmjitrunners def resultFile(name=None): """Where a simulator writes its results. @@ -618,18 +633,19 @@ def resultFile(name=None): """ if not name: return "%s_res.%s" % (conf["fileName"], outputFormat) - extension = shared.fmiSimulator(name)["resultExtension"] - if len(fmisimulators) < 2: + # A wasm-jit runner is omc, which writes the format the model was translated for. + extension = shared.fmiSimulator(name)["resultExtension"] if fmisimulators else outputFormat + if len(runners) < 2: return "%s_res.%s" % (conf["fileName"], extension) return "%s_%s_res.%s" % (conf["fileName"], name, extension) def artifactPrefix(name=None): """The files a simulator's results are written to, under files/.""" - if not name or len(fmisimulators) < 2: + if not name or len(runners) < 2: return os.path.abspath("../files/%s" % conf["fileName"]).replace('\\','/') return os.path.abspath("../files/%s_%s" % (conf["fileName"], name)).replace('\\','/') -resFile = resultFile(fmisimulators[0][0]) if fmisimulators else resultFile() +resFile = resultFile(runners[0][0]) if runners else resultFile() def simulateFmu(name, command, resFile, simFile): """Run the FMU with one simulator, writing what it says to simFile.""" @@ -651,9 +667,44 @@ def simulateFmu(name, command, resFile, simFile): return checkOutputTimeout("(rm -f %s.pipe ; mkfifo %s.pipe ; head -c 1048576 < %s.pipe >> %s & %s > %s.pipe 2>&1)" % (pipe,pipe,pipe,simFile,cmd,pipe), 1.05*conf["ulimitExe"], conf) +def artifactCmd(runnerFlags, resFile): + """The simulate() that runs the exported artifact one way. + + Nothing is translated: `resimulateExecutable` points at the artifact, `-s + fmi3:...` picks which of its interfaces runs, and the experiment comes from + the flags rather than from what the export baked in. + """ + # An empty output format is a run with nothing to compare against, so it is + # asked for no result file at all rather than one nobody reads. + resultArgument = "-noemit" if outputFormat == "empty" else "-r=%s" % resFile + simflags = " ".join(x for x in (annotationSimFlags, conf["simFlags"], emit_protected, + "-lv LOG_STATS", + "-startTime=%g -stopTime=%g -tolerance=%g -stepSize=%g" % (startTime,stopTime,tolerance,stepSize), + resultArgument, runnerFlags) if x.strip()) + return 'simulate(%s,startTime=%g,stopTime=%g,tolerance=%g,numberOfIntervals=%d,outputFormat="%s",variableFilter="%s",fileNamePrefix="%s",simflags="%s",resimulateExecutable="%s.fmu")' % ( + conf["modelName"],startTime,stopTime,tolerance,numberOfIntervals,outputFormat,variableFilter,conf["fileName"],simflags,conf["fileName"].replace(".","_")) + +def simulateArtifact(name, runnerFlags, resFile, simFile): + """Run the artifact one way, writing what omc says to simFile. + + Returns what simulate() answered; an empty resultFile is a failed run. + """ + cmd = artifactCmd(runnerFlags, resFile) + # The export is a directory, and a zipped one unpacks itself beside the .fmu on + # the first run; the cleanup removes what this file names. + with open("%s.tmpfiles" % conf["fileName"], "a+") as fp: + fp.write("%s.fmu\n%s_artifact\n" % (conf["fileName"].replace(".","_"), conf["fileName"].replace(".","_"))) + with open(simFile, "w") as fp: + fp.write("startTime=%g\nstopTime=%g\ntolerance=%g\nnumberOfIntervals=%d\nstepSize=%g\n" % (startTime,stopTime,tolerance,numberOfIntervals,stepSize)) + fp.write("wasm artifact (%s: %s): %s\n" % (name, shared.wasmJitRunner(name).get("description") or "", cmd)) + res = sendExpressionTimeout(omc, cmd, conf["ulimitExe"]) or {} + with open(simFile, "a+") as fp: + fp.write(res.get("messages") or "") + return res + def simElapsed(): - # omc's own time: the wall clock here covers the wrong run for both flags - if useSimulate or useColdHot: + # omc's own time: the wall clock here covers the wrong run for these flags + if useSimulate or useColdHot or useArtifact: return (simres or {}).get("timeSimulation") or 0.0 return monotonic()-start @@ -670,6 +721,13 @@ def simElapsed(): writeResultAndExit(0, False, omc, omc_new) (name, command) = fmisimulators[0] res = simulateFmu(name, command, resFile, simFile) + elif useArtifact: + (name, runnerFlags) = wasmjitrunners[0] + simres = simulateArtifact(name, runnerFlags, resFile, simFile) + if not simres.get("resultFile"): + # The same shape a failing FMI simulator takes: the handler below decides + # whether the other runners of this artifact still get their turn. + raise TimeoutError("%s failed to simulate the wasm artifact" % name) elif isWasmJit: if not useSimulate: cmd = simulateCmd(resimulate=True) @@ -726,7 +784,7 @@ def simElapsed(): execstat["sim"] = monotonic()-start # checkOutputTimeout raises TimeoutError for a command that fails as well as # for one that runs out of time, so this covers both. - if len(fmisimulators) > 1: + if len(runners) > 1: # The FMU is built and the other simulators are about to run it. What the # first one did says nothing about them, so record the failure and let them # have their turn. Ending the model here would write them all down as having @@ -734,8 +792,8 @@ def simElapsed(): # below already avoids for a failure in any simulator but the first. firstSimulatorFailed = True with open(errFile, 'a+') as fp: - fp.write("%s failed or timed out simulating the FMU; the other simulators still get to run it\n" - % fmisimulators[0][0]) + fp.write("%s failed or timed out simulating it; the others still get to run it\n" + % runners[0][0]) else: writeResultAndExit(0, True, omc, omc_new) @@ -852,27 +910,34 @@ def verifyAgainstReference(resFile, prefix, stat): # The first simulator's results are the ones every non-FMI code path expects. # There is nothing to compare when it never produced any. if not firstSimulatorFailed: - verifyAgainstReference(resFile, artifactPrefix(fmisimulators[0][0] if fmisimulators else None) + ".diff", execstat) + verifyAgainstReference(resFile, artifactPrefix(runners[0][0] if runners else None) + ".diff", execstat) # The FMU is built; every other simulator the job asked for is now only a # simulation and a comparison. A tool that times out or fails takes its own # results down with it and leaves the others alone - the first one included, # see the TimeoutError handler above. -for (name, command) in fmisimulators[1:]: +for (name, command) in runners[1:]: stat = {"sim": None, "diff": None, "phase": 5} simulators[name] = stat simFileOther = os.path.abspath("../files/%s_%s.sim" % (conf["fileName"], name)).replace('\\','/') other = resultFile(name) start = monotonic() try: - simulateFmu(name, command, other, simFileOther) - stat["sim"] = monotonic()-start + if useArtifact: + res = simulateArtifact(name, command, other, simFileOther) + stat["sim"] = res.get("timeSimulation") or (monotonic()-start) + if not res.get("resultFile"): + writeResult() + continue + else: + simulateFmu(name, command, other, simFileOther) + stat["sim"] = monotonic()-start stat["phase"] = 6 verifyAgainstReference(other, artifactPrefix(name) + ".diff", stat) except TimeoutError as e: stat["sim"] = monotonic()-start with open(errFile, 'a+') as fp: - fp.write("%s timed out simulating the FMU\n" % name) + fp.write("%s timed out simulating the %s\n" % (name, "artifact" if useArtifact else "FMU")) writeResult() # quit omc_new: every verification needed it diff --git a/testresults.md b/testresults.md index c1bfa22..51467f3 100644 --- a/testresults.md +++ b/testresults.md @@ -20,6 +20,12 @@ simulation with FMI and C runtime - [FMI C++](https://libraries.openmodelica.org/branches/overview-c++.html): simulation with FMI and C++ runtime + - [wasm-jit](https://libraries.openmodelica.org/branches/overview-wasm-jit.html): + simulation with the WebAssembly runtime. Each model is exported once as a + wasm artifact and simulated three ways — `wasm-jit` runs the simulation + runtime inside it, `wasm-jit-me` its FMI 3.0 Model Exchange interface and + `wasm-jit-cs` its FMI 3.0 Co-Simulation one — so the three differ only in + how the same compiled model is driven - **Regression reports and history plots**: Regression reports are periodically generated, using the latest development