Skip to content

Commit

Permalink
Add async compilation (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
zemlyansky committed Nov 25, 2021
1 parent afeefc4 commit f4b038a
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 150 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ EMCFLAGS = -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_FUNCTIONS=$(EXPORTED_FUNCTIONS)

build:
${CC} ${CFLAGS} ${EMCFLAGS} ${FILES} src/api.c -o wasm/native.js -s BINARYEN_ASYNC_COMPILATION=0;
mv wasm/native.js wasm/native-sync.js
${CC} ${CFLAGS} ${EMCFLAGS} ${FILES} src/api.c -o wasm/native.js -s BINARYEN_ASYNC_COMPILATION=1;
mv wasm/native.js wasm/native-async.js

19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ arima.train(ts, exog) // or arima.fit(ts, exog)
arima.predict(10, exognew) // Predict 10 steps forwars using new exogenous variables
```

### Running in browsers
As described in the issue [#10](https://github.com/zemlyansky/arima/issues/10) Chrome prevents compilation of wasm modules >4kB.
There are two ways to overcome this:
- Load `arima` in a [Web Worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)
- Use the `arima/async` module

Example of async loading:
```javascript
const ARIMAPromise = require('arima/async')

ARIMAPromise.then(ARIMA => {
const ts = Array(10).fill(0).map((_, i) => i + Math.random() / 5)
const arima = new ARIMA({ p: 2, d: 1, q: 2, P: 0, D: 0, Q: 0, S: 0, verbose: false }).train(ts)
const [pred, errors] = arima.predict(10)
})
```
All following examples use **synchronous** compilation (Node.js, Firefox). They will not work in Chrome.


### Example: ARIMA
```javascript
// Load package
Expand Down
9 changes: 9 additions & 0 deletions async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Async compilation ('arima/async')

const Module = require('./wasm/native-async.js')
const bin = require('./wrapper/native.bin.js')
const loadARIMA = require('./load.js')

const modulePromise = Module({ wasmBinary: bin })

module.exports = modulePromise.then(loadARIMA)
6 changes: 6 additions & 0 deletions example/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
To bundle sync version: (doesn't work in Chrome's main thread):
`browserify example-browser-sync.js -o bundle.js`

Bundle async version:
`browserify example-browser-async.js -o bundle.js`

20 changes: 20 additions & 0 deletions example/browser/example-browser-async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const ARIMAPromise = require('../../async') // Change '../../async' to 'arima/async'

ARIMAPromise.then(ARIMA => {
const ts = Array(10).fill(0).map((_, i) => i + Math.random() / 5)
const arima = new ARIMA({ p: 2, d: 1, q: 2, P: 0, D: 0, Q: 0, S: 0, verbose: false }).train(ts)
const [pred, errors] = arima.predict(10)

document.getElementById('output').innerText = `
Async compilation
Data:
${ts.join('\n')}
Predictions:
${pred.join('\n')}
Errors:
${errors.join('\n')}
`
})
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const ARIMA = require('../../')
const ARIMA = require('../../') // Change '../../' to 'arima'

const ts = Array(10).fill(0).map((_, i) => i + Math.random() / 5)
const arima = new ARIMA({ p: 2, d: 1, q: 2, P: 0, D: 0, Q: 0, S: 0, verbose: false }).train(ts)
const [pred, errors] = arima.predict(10)

document.getElementById('output').innerText = `
Sync compilation
Data:
${ts.join('\n')}
Expand Down
1 change: 0 additions & 1 deletion example/browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
</head>
<body>
<h1>ARIMA. Browser example</h1>
<pre>browserify example_browser.js -o bundle.js</pre>
<pre id="output"></pre>
<script src="bundle.js"></script>
</body>
Expand Down
File renamed without changes.
154 changes: 6 additions & 148 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,151 +1,9 @@
const Module = require('./wasm/native.js')
const bin = require('./wrapper/native.bin.js')
const m = Module({ wasmBinary: bin })

const _fit_sarimax = m.cwrap('fit_sarimax', 'number', ['array', 'array', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'boolean'])
const _predict_sarimax = m.cwrap('predict_sarimax', 'number', ['number', 'array', 'array', 'array', 'number'])
const _fit_autoarima = m.cwrap('fit_autoarima', 'number', ['array', 'array', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'boolean'])
const _predict_autoarima = m.cwrap('predict_autoarima', 'number', ['number', 'array', 'array', 'array', 'number'])

function uintify (arr) {
return new Uint8Array(Float64Array.from(arr).buffer)
}

function flat (arr) {
return [].concat.apply([], arr)
}

function transpose (arr) {
return arr[0].map((x, i) => arr.map(x => x[i]))
}

function prepare (arr) {
const farr = flat(arr)
for (let i = 0; i < farr.length - 2; i++) {
if (isNaN(farr[i + 1])) {
farr[i + 1] = farr[i]
}
}
return farr
}

function getResults (addr, l) {
const res = [[], []]
for (let i = 0; i < l * 2; i++) {
res[i < l ? 0 : 1].push(m.HEAPF64[addr / Float64Array.BYTES_PER_ELEMENT + i])
}
return res
}
// Sync compilation (default)

const defaults = {
method: 0,
optimizer: 6,
s: 0,
verbose: true,
transpose: false,
auto: false,
approximation: 1,
search: 1
}

const params = {
p: 1,
d: 0,
q: 1,
P: 0,
D: 0,
Q: 0
}

const paramsAuto = {
p: 5,
d: 2,
q: 5,
P: 2,
D: 1,
Q: 2
}

function ARIMA () {
// Preserve the old functional API: ARIMA(ts, len, opts)
if (!(this instanceof ARIMA)) {
console.warn('Calling ARIMA as a function will be deprecated in the future')
return (new ARIMA(arguments[2])).train(arguments[0]).predict(arguments[1])
}
// A new, class API has opts as the only argument here: new ARIMA (opts)
const opts = arguments[0]
const o = Object.assign({}, defaults, opts.auto ? paramsAuto : params, opts)
if (Math.min(o.method, o.optimizer, o.p, o.d, o.q, o.P, o.D, o.Q, o.s) < 0) {
throw new Error('Model parameter can\'t be negative')
}
if ((o.P + o.D + o.Q) === 0) {
o.s = 0
} else if (o.s === 0) {
o.P = o.D = o.Q = 0
}
this.options = o
}

ARIMA.prototype.train = function (ts, exog = []) {
const o = this.options
if (o.transpose && Array.isArray(exog[0])) {
exog = transpose(exog)
}
this.ts = uintify(prepare(ts))
this.exog = uintify(prepare(exog))
this.lin = ts.length
this.nexog = exog.length > 0 ? (Array.isArray(exog[0]) ? exog.length : 1) : 0
this.model = o.auto
? _fit_autoarima(
this.ts, this.exog,
o.p, o.d, o.q,
o.P, o.D, o.Q, o.s,
this.nexog,
this.lin,
o.method,
o.optimizer,
o.approximation,
o.search,
o.verbose
)
: _fit_sarimax(
this.ts, this.exog,
o.p, o.d, o.q,
o.P, o.D, o.Q, o.s,
this.nexog,
this.lin,
o.method,
o.optimizer,
o.verbose
)
return this
}

ARIMA.prototype.fit = function (...a) {
return this.train(...a)
}
const Module = require('./wasm/native-sync.js')
const bin = require('./wrapper/native.bin.js')
const loadARIMA = require('./load.js')

ARIMA.prototype.predict = function (l, exog = []) {
const o = this.options
if (o.transpose && Array.isArray(exog[0])) {
exog = transpose(exog)
}
const addr = o.auto
? _predict_autoarima(
this.model,
this.ts,
this.exog, // old
uintify(prepare(exog)), // new
l
)
: _predict_sarimax(
this.model,
this.ts,
this.exog, // old
uintify(prepare(exog)), // new
l
)
return getResults(addr, l)
}
const moduleObject = Module({ wasmBinary: bin })

module.exports = ARIMA
module.exports = loadARIMA(moduleObject)

0 comments on commit f4b038a

Please # to comment.