Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .CI/Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -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/<N>/merge - the pull request as it would land - the results fill a pr-<N> table, and the report compares them against the newest run of pull_request_baseline. Left empty, nothing of this runs.')
Expand Down Expand Up @@ -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')
}
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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"
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 `<model>.fmu` as a
directory holding the model description and the model kernel. Each runner then
simulates it through `simulate(..., resimulateExecutable="<model>.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 `-<name>` 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
Expand Down
29 changes: 29 additions & 0 deletions configs/wasm-jit-runners.json
Original file line number Diff line number Diff line change
@@ -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\", <this machine>})",
"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=\"<model>.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 -<name>, 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"
}
}
40 changes: 40 additions & 0 deletions shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 -<name>."""
return branch + wasmJitRunner(name).get("branchSuffix", "-%s" % name)

def branchForSimulator(branch, name):
"""Where the results of one FMI simulator of a run are stored.

Expand Down
23 changes: 18 additions & 5 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <branch>-<name>, 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,<this machine>}) 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 <branch>-<name>, 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")
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading