diff --git a/.gitignore b/.gitignore index 4e6d578..42680c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target /html/opentally.js -/html/opentally_bg.wasm +/html/opentally_*.wasm diff --git a/build_wasm.sh b/build_wasm.sh index 0b43028..e569d50 100755 --- a/build_wasm.sh +++ b/build_wasm.sh @@ -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 diff --git a/html/index.html b/html/index.html index d4519c2..eb7c822 100644 --- a/html/index.html +++ b/html/index.html @@ -295,7 +295,6 @@
Printing directly from this page is not supported. Use the ‘Print result’ button to generate a printer-friendly report.
- diff --git a/html/worker.js b/html/worker.js index 9123168..c1b291e 100644 --- a/html/worker.js +++ b/html/worker.js @@ -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; } } diff --git a/src/ties.rs b/src/ties.rs index 64c8820..fce34d0 100644 --- a/src/ties.rs +++ b/src/ties.rs @@ -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; + fn get_user_input(s: &str) -> Option; } #[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::() { - 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::() { + 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!(); + } } } }