|
function getWorkerURLFromFunction(fn) { |
|
var bodyPattern = /\'WORKER_START\'((\s+)?(.*(\s+)?)*)\'WORKER_END\'/ |
|
var content = fn.toString().match(bodyPattern)[1] |
|
var blob = new Blob([content], {type: 'text/javascript'}) |
|
return URL.createObjectURL(blob) |
|
} |
|
|
|
function workerThread() { |
|
'WORKER_START' |
|
|
|
importScripts('http://cdnjs.cloudflare.com/ajax/libs/mathjs/3.1.0/math.min.js') |
|
|
|
self.addEventListener('message', function (event) { |
|
var message = event.data |
|
self.postMessage({ |
|
output: pi(message.digits, message.precision, message.index, message.tail), |
|
digits: message.digits, |
|
precision: message.precision, |
|
index: message.index, |
|
tail: message.tail, |
|
id: message.id, |
|
timestamp: Date.now() |
|
}) |
|
}, false) |
|
|
|
var add = math.add |
|
var multiply = math.multiply |
|
var divide = math.divide |
|
var factorial = math.factorial |
|
var pow = math.pow |
|
var bignumber = math.bignumber |
|
|
|
// Uses Newton's approximation |
|
// https://tr.wikipedia.org/wiki/Pi_say%C4%B1s%C4%B1#cite_ref-6 |
|
function pi (digits, sliceSize, index, tail) { |
|
math.config({ precision: digits }) |
|
var sliceTop = sliceSize * (index + 1) + tail |
|
var sliceBottom = sliceSize * index |
|
var total = 0 |
|
while (sliceTop-- > sliceBottom) { |
|
let n = bignumber(sliceTop) |
|
total = add( |
|
total, |
|
divide( |
|
bignumber( |
|
multiply( |
|
pow( 2, add(n, 1) ), |
|
pow( factorial(n), 2 ))), |
|
factorial( |
|
add( multiply(2, n ), 1 )))) |
|
} |
|
return total.toString() |
|
} |
|
|
|
'WORKER_END' |
|
} |
|
|
|
// Concurrency |
|
var Api = { |
|
|
|
init(options) { |
|
this.onmessage = options.onmessage || new Function() |
|
this.spawn() |
|
return this |
|
}, |
|
|
|
_divisableByCoreCount(n) { |
|
if (!(n % this.cores)) return n |
|
return this._divisableByCoreCount(n - 1) |
|
}, |
|
|
|
cores: navigator.hardwareConcurrency || navigator.cores || 4, |
|
workers: [], |
|
pending: {}, |
|
resolved: {}, |
|
|
|
// Spawn as many workers as cpu cores. |
|
spawn() { |
|
this.workers = new Array(this.cores).join(' ').split(' ').map(e => ( |
|
new Worker(getWorkerURLFromFunction(workerThread)) |
|
)) |
|
}, |
|
|
|
makeRequest(message) { |
|
this.pending[message.id] = this.pending[message.id] || [] |
|
this.pending[message.id].push(message) |
|
|
|
this.workers.forEach((worker, index, workers) => { |
|
var messageCopy = JSON.parse(JSON.stringify(message)) |
|
var precision = this._divisableByCoreCount(message.precision) |
|
var diff = index + 1 === workers.length ? |
|
message.precision - precision : 0 |
|
messageCopy.precision = precision / this.cores |
|
messageCopy.tail = diff |
|
messageCopy.index = index |
|
worker.postMessage(messageCopy) |
|
worker.onmessage = this.onReceiveMessage.bind(this) |
|
}) |
|
}, |
|
|
|
onReceiveMessage(event) { |
|
var message = event.data |
|
this.pending[message.id].pop() |
|
this.resolved[message.id] = this.resolved[message.id] || [] |
|
this.resolved[message.id].push(message) |
|
|
|
if (this.resolved[message.id].length === this.cores) { |
|
math.config({ precision: message.digits }) |
|
this.onmessage( |
|
this.resolved[message.id].reduce((memo, curr) => { |
|
return Object.assign(memo, curr, { |
|
output: math.add( |
|
math.bignumber(memo.output || 0), |
|
math.bignumber(curr.output) |
|
).toString() |
|
}) |
|
}, {}) |
|
) |
|
} |
|
} |
|
} |
|
|
|
var App = { |
|
|
|
UI: { |
|
digits: document.getElementById('app-form-digits'), |
|
precision: document.getElementById('app-form-precision'), |
|
output: document.getElementById('app-output'), |
|
container: document.getElementById('app-form-container') |
|
}, |
|
|
|
state: { |
|
digits: 100, |
|
precision: 300, |
|
lastMessageId: null, |
|
lastReceivedMessageId: null, |
|
output: '' |
|
}, |
|
|
|
setState(newState) { |
|
Object.keys(newState).forEach(key => { |
|
this.state[key] = newState[key] |
|
}) |
|
this.render() |
|
}, |
|
|
|
bindEvents() { |
|
this.UI.digits.addEventListener( |
|
'input', this.onChangeDigits.bind(this)) |
|
|
|
this.UI.precision.addEventListener( |
|
'input', this.onChangePrecision.bind(this)) |
|
}, |
|
|
|
onMessageReceived(message) { |
|
console.log('took (ms)', message.timestamp - (+message.id.split('_')[0])) |
|
this.setState({ |
|
lastReceivedMessageId: message.id, |
|
output: message.output |
|
}) |
|
}, |
|
|
|
makeUpdateRequest() { |
|
var id = `${ Date.now() }_${ Math.random() * 999 }` |
|
this.setState({ lastMessageId: id }) |
|
this.api.makeRequest({ id, |
|
digits: this.state.digits, |
|
precision: this.state.precision |
|
}) |
|
}, |
|
|
|
onChangeDigits(event) { |
|
this.setState({ |
|
digits: parseInt(event.target.value, 10) |
|
}) |
|
this.makeUpdateRequest() |
|
}, |
|
|
|
onChangePrecision(event) { |
|
this.setState({ |
|
precision: parseInt(event.target.value, 10) |
|
}) |
|
this.makeUpdateRequest() |
|
}, |
|
|
|
init() { |
|
this.UI.digits.value = this.state.digits |
|
this.UI.precision.value = this.state.precision |
|
this.bindEvents() |
|
this.api = Api.init({ |
|
onmessage: this.onMessageReceived.bind(this) |
|
}) |
|
this.makeUpdateRequest() |
|
}, |
|
|
|
render() { |
|
// Update feedback message |
|
var feedbackClass = 'working' |
|
var { lastMessageId, lastReceivedMessageId } = this.state |
|
if (lastMessageId === lastReceivedMessageId) { |
|
this.UI.container.classList.remove(feedbackClass) |
|
} else { |
|
this.UI.container.classList.add(feedbackClass) |
|
} |
|
|
|
// Update output |
|
this.UI.output.innerText = this.state.output |
|
} |
|
|
|
} |
|
|
|
App.init() |