Use Asyncify to process ties in web UI

This commit is contained in:
RunasSudo 2021-07-27 22:57:53 +10:00
parent a64110b6a1
commit a5a61731b5
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
5 changed files with 86 additions and 43 deletions

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
/target
/html/opentally.js
/html/opentally_bg.wasm
/html/opentally_*.wasm

View File

@ -1,3 +1,19 @@
#!/bin/sh
PATH=$PATH:$HOME/.cargo/bin
# Build cargo
PROFILE=${1:-release}
cargo build --lib --target wasm32-unknown-unknown --$PROFILE && /home/runassudo/.cargo/bin/wasm-bindgen --target no-modules target/wasm32-unknown-unknown/$PROFILE/opentally.wasm --out-dir html --no-typescript
if [ $PROFILE == 'debug' ]; then
cargo build --lib --target wasm32-unknown-unknown
else
cargo build --lib --target wasm32-unknown-unknown --$PROFILE
fi
# Apply wasm-bindgen
wasm-bindgen --target no-modules target/wasm32-unknown-unknown/$PROFILE/opentally.wasm --out-dir html --no-typescript
# Apply Asyncify
MANGLED=$(wasm-dis html/opentally_bg.wasm | grep '(import "wbg" "__wbg_getuserinput_' | awk '{print $3;}' | tr -d '"')
wasm-opt -O2 --asyncify --pass-arg asyncify-imports@wbg.$MANGLED html/opentally_bg.wasm -o html/opentally_async.wasm
rm html/opentally_bg.wasm

View File

@ -295,7 +295,6 @@
<div id="printWarning">Printing directly from this page is not supported. Use the ‘Print result’ button to generate a printer-friendly report.</div>
<script src="vendor/vanilla-js-dropdown.min.js"></script>
<script src="opentally.js?v=GITVERSION"></script>
<script src="index.js?v=GITVERSION"></script>
</body>
</html>

View File

@ -1,9 +1,18 @@
importScripts('opentally.js');
var wasm = wasm_bindgen;
var wasmRaw;
// For asyncify
const DATA_ADDR = 16;
const DATA_START = DATA_ADDR + 8;
const DATA_END = 50 * 1024; // Needs to be increased compared with Asyncify default
async function initWasm() {
await wasm_bindgen('opentally_bg.wasm');
wasmRaw = await wasm_bindgen('opentally_async.wasm');
new Int32Array(wasmRaw.memory.buffer, DATA_ADDR).set([DATA_START, DATA_END]);
postMessage({'type': 'init', 'version': wasm.version()});
}
initWasm();
@ -59,27 +68,30 @@ onmessage = function(evt) {
stageNum = 2;
resume_count();
resumeCount();
} else if (evt.data.type == 'userInput') {
user_input_buffer = evt.data.response;
resume_count();
userInputBuffer = evt.data.response;
// Rewind the stack
// Asyncify will retrace the function calls in the stack until again reaching get_user_input
wasmRaw.asyncify_start_rewind(DATA_ADDR);
resumeCount();
}
}
function resume_count() {
function resumeCount() {
for (;; stageNum++) {
try {
let isDone = wasm['count_one_stage_' + numbers](state, opts);
if (isDone) {
break;
}
} catch (ex) {
if (ex === "RequireInput") {
return;
} else {
throw ex;
}
let isDone = wasm['count_one_stage_' + numbers](state, opts);
if (unwindingStack) {
// This stage caused a stack unwind in get_user_input so ignore the result
// We will resume execution when a userInput message is received
return;
}
if (isDone) {
break;
}
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)});
@ -90,15 +102,27 @@ function resume_count() {
postMessage({'type': 'finalResultSummary', 'summary': wasm['final_result_summary_' + numbers](state, opts)});
}
var user_input_buffer = null;
var unwindingStack = false;
var userInputBuffer = null;
function read_user_input_buffer(message) {
if (user_input_buffer === null) {
function get_user_input(message) {
if (userInputBuffer === null) {
postMessage({'type': 'requireInput', 'message': message});
// Record the current state of the stack
wasmRaw.asyncify_start_unwind(DATA_ADDR);
unwindingStack = true;
// No further WebAssembly will be executed and control will return to resumeCount
return null;
} else {
let user_input = user_input_buffer;
user_input_buffer = null;
return user_input;
// We have reached the point the stack was originally unwound, so resume normal execution
unwindingStack = false;
wasmRaw.asyncify_stop_rewind();
// Return the correct result to WebAssembly
let userInput = userInputBuffer;
userInputBuffer = null;
return userInput;
}
}

View File

@ -198,7 +198,6 @@ where
}
/// Prompt the candidate for input, depending on CLI or WebAssembly target
// FIXME: This may have unexpected behaviour if the tie occurs in the middle of a stage
#[cfg(not(target_arch = "wasm32"))]
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
println!("Multiple tied candidates:");
@ -231,7 +230,7 @@ fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
extern "C" {
fn read_user_input_buffer(s: &str) -> Option<String>;
fn get_user_input(s: &str) -> Option<String>;
}
#[cfg(target_arch = "wasm32")]
@ -242,25 +241,30 @@ fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError
}
message.push_str(&format!("Which candidate to select? [1-{}] ", candidates.len()));
match read_user_input_buffer(&message) {
Some(response) => {
match response.trim().parse::<usize>() {
Ok(val) => {
if val >= 1 && val <= candidates.len() {
return Ok(candidates[val - 1]);
} else {
let _ = read_user_input_buffer(&message);
return Err(STVError::RequireInput);
loop {
let response = get_user_input(&message);
match response {
Some(response) => {
match response.trim().parse::<usize>() {
Ok(val) => {
if val >= 1 && val <= candidates.len() {
return Ok(candidates[val - 1]);
} else {
// Invalid selection
continue;
}
}
Err(_) => {
// Invalid selection
continue;
}
}
Err(_) => {
let _ = read_user_input_buffer(&message);
return Err(STVError::RequireInput);
}
}
}
None => {
return Err(STVError::RequireInput);
None => {
// No available user input in buffer - stack will be unwound
unreachable!();
}
}
}
}