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
No known key found for this signature in database
GPG Key ID: 7234E476BF21C61A
5 changed files with 86 additions and 43 deletions

2
.gitignore vendored
View File

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

View File

@ -1,3 +1,19 @@
#!/bin/sh #!/bin/sh
PATH=$PATH:$HOME/.cargo/bin
# Build cargo
PROFILE=${1:-release} 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> <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="vendor/vanilla-js-dropdown.min.js"></script>
<script src="opentally.js?v=GITVERSION"></script>
<script src="index.js?v=GITVERSION"></script> <script src="index.js?v=GITVERSION"></script>
</body> </body>
</html> </html>

View File

@ -1,9 +1,18 @@
importScripts('opentally.js'); importScripts('opentally.js');
var wasm = wasm_bindgen; 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() { 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()}); postMessage({'type': 'init', 'version': wasm.version()});
} }
initWasm(); initWasm();
@ -59,27 +68,30 @@ onmessage = function(evt) {
stageNum = 2; stageNum = 2;
resume_count(); resumeCount();
} else if (evt.data.type == 'userInput') { } else if (evt.data.type == 'userInput') {
user_input_buffer = evt.data.response; userInputBuffer = evt.data.response;
resume_count();
// 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++) { for (;; stageNum++) {
try { let isDone = wasm['count_one_stage_' + numbers](state, opts);
let isDone = wasm['count_one_stage_' + numbers](state, opts);
if (isDone) { if (unwindingStack) {
break; // This stage caused a stack unwind in get_user_input so ignore the result
} // We will resume execution when a userInput message is received
} catch (ex) { return;
if (ex === "RequireInput") { }
return;
} else { if (isDone) {
throw ex; break;
}
} }
postMessage({'type': 'updateResultsTable', 'result': wasm['update_results_table_' + numbers](stageNum, state, opts)}); 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)}); 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) { function get_user_input(message) {
if (user_input_buffer === null) { if (userInputBuffer === null) {
postMessage({'type': 'requireInput', 'message': message}); 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; return null;
} else { } else {
let user_input = user_input_buffer; // We have reached the point the stack was originally unwound, so resume normal execution
user_input_buffer = null; unwindingStack = false;
return user_input; 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 /// 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"))] #[cfg(not(target_arch = "wasm32"))]
fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> { fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError> {
println!("Multiple tied candidates:"); println!("Multiple tied candidates:");
@ -231,7 +230,7 @@ fn prompt<'c>(candidates: &Vec<&'c Candidate>) -> Result<&'c Candidate, STVError
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
fn read_user_input_buffer(s: &str) -> Option<String>; fn get_user_input(s: &str) -> Option<String>;
} }
#[cfg(target_arch = "wasm32")] #[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())); message.push_str(&format!("Which candidate to select? [1-{}] ", candidates.len()));
match read_user_input_buffer(&message) { loop {
Some(response) => { let response = get_user_input(&message);
match response.trim().parse::<usize>() {
Ok(val) => { match response {
if val >= 1 && val <= candidates.len() { Some(response) => {
return Ok(candidates[val - 1]); match response.trim().parse::<usize>() {
} else { Ok(val) => {
let _ = read_user_input_buffer(&message); if val >= 1 && val <= candidates.len() {
return Err(STVError::RequireInput); 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 => {
None => { // No available user input in buffer - stack will be unwound
return Err(STVError::RequireInput); unreachable!();
}
} }
} }
} }