Skip to content

Commit

Permalink
Keep query between runs when we can in the playground, for performanc…
Browse files Browse the repository at this point in the history
…e. (#560)

* Static query state.

* Logic rework in the React app.

* Comments and cleanup.

* Update test.
  • Loading branch information
torhovland authored Jul 13, 2023
1 parent ee3ed93 commit fc03b3f
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 67 deletions.
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 60 additions & 32 deletions topiary-playground/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
#[cfg(target_arch = "wasm32")]
use topiary::{formatter, Configuration, FormatterResult, Operation, TopiaryQuery};
use std::sync::Mutex;
#[cfg(target_arch = "wasm32")]
use topiary::{formatter, Configuration, FormatterResult, Language, Operation, TopiaryQuery};
#[cfg(target_arch = "wasm32")]
use tree_sitter_facade::TreeSitter;
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

#[cfg(target_arch = "wasm32")]
struct QueryState {
language: Language,
grammar: tree_sitter_facade::Language,
query: TopiaryQuery,
}

#[cfg(target_arch = "wasm32")]
/// The query state is stored in a static variable, so the playground can reuse
/// it across multiple runs as long as it doesn't change.
static QUERY_STATE: Mutex<Option<QueryState>> = Mutex::new(None);

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(js_name = topiaryInit)]
pub async fn topiary_init() -> Result<(), JsError> {
Expand All @@ -17,55 +31,69 @@ pub async fn topiary_init() -> Result<(), JsError> {
TreeSitter::init().await
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(js_name = queryInit)]
pub async fn query_init(query_content: String, language_name: String) -> Result<(), JsError> {
let language_normalized = language_name.replace('-', "_");
let configuration = Configuration::parse_default_configuration()?;
let language = configuration.get_language(language_normalized)?.clone();
let grammar = language.grammar_wasm().await?;
let query = TopiaryQuery::new(&grammar, &query_content)?;

let mut guard = QUERY_STATE.lock().unwrap();

*guard = Some(QueryState {
language,
grammar,
query,
});

Ok(())
}

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub async fn format(
input: &str,
query: &str,
language: &str,
check_idempotence: bool,
tolerate_parsing_errors: bool,
) -> Result<String, JsError> {
let language_normalized = language.replace('-', "_");
format_inner(
input,
query,
language_normalized.as_str(),
check_idempotence,
tolerate_parsing_errors,
)
.await
.map_err(|e| format_error(&e))
format_inner(input, check_idempotence, tolerate_parsing_errors)
.await
.map_err(|e| format_error(&e))
}

#[cfg(target_arch = "wasm32")]
async fn format_inner(
input: &str,
query_content: &str,
language_name: &str,
check_idempotence: bool,
tolerate_parsing_errors: bool,
) -> FormatterResult<String> {
let mut output = Vec::new();

let configuration = Configuration::parse_default_configuration()?;
let language = configuration.get_language(language_name)?;
let grammar = language.grammar_wasm().await?;
let query = TopiaryQuery::new(&grammar, query_content)?;
let mut guard = QUERY_STATE.lock().unwrap();

formatter(
&mut input.as_bytes(),
&mut output,
&query,
language,
&grammar,
Operation::Format {
skip_idempotence: !check_idempotence,
tolerate_parsing_errors,
},
)?;

Ok(String::from_utf8(output)?)
match &mut *guard {
Some(query_state) => {
formatter(
&mut input.as_bytes(),
&mut output,
&query_state.query,
&query_state.language,
&query_state.grammar,
Operation::Format {
skip_idempotence: !check_idempotence,
tolerate_parsing_errors,
},
)?;

Ok(String::from_utf8(output)?)
}
None => Err(topiary::FormatterError::Internal(
"The query has not been initialized.".into(),
None,
)),
}
}

#[cfg(target_arch = "wasm32")]
Expand Down
2 changes: 1 addition & 1 deletion web-playground/e2e/sample-tester.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ async function readOutput() {

// Wait for useful output.
await page.waitForFunction(
el => el?.textContent !== "" && el?.textContent !== "Formatting ...",
el => el?.textContent !== "" && el?.textContent !== "Formatting ..." && el?.textContent !== "Compiling query ...",
{ polling: "mutation", timeout: 30000 },
el
);
Expand Down
112 changes: 85 additions & 27 deletions web-playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@ import useDebounce from "./hooks/useDebounce";
import languages from './samples/languages_export';
import init, {
topiaryInit,
queryInit,
format,
} from "./wasm-app/topiary_playground.js";
import "./App.css";

const debounceDelay = 500;

function App() {
const [isInitialised, setIsInitialised] = useState(false);
const initCalled = useRef(false);
const defaultLanguage = "json";
const defaultQuery = languages[defaultLanguage].query;
const defaultInput = languages[defaultLanguage].input;

// These don't have to be useState, as they don't need to trigger UI changes.
const initCalled = useRef(false);
const isQueryCompiling = useRef(false);
const queryChanged = useRef(true);
const previousDebouncedInput = useRef("");
const previousDebouncedQuery = useRef("");
const previousIsInitialised = useRef(false);

const [isInitialised, setIsInitialised] = useState(false);
const [languageOptions, setLanguageOptions] = useState([] as ReactElement[]);
const [currentLanguage, setCurrentLanguage] = useState(defaultLanguage);
const [onTheFlyFormatting, setOnTheFlyFormatting] = useState(true);
Expand All @@ -31,35 +40,15 @@ function App() {
const debouncedInput = useDebounce(input, debounceDelay);
const debouncedQuery = useDebounce(query, debounceDelay);

const runFormat = useCallback((i: string, q: string) => {
const outputFormat = async () => {
try {
const start = performance.now();
setOutput(await format(i, q, currentLanguage, idempotence, tolerateParsingErrors));
setProcessingTime(performance.now() - start);
} catch (e) {
setOutput(String(e));
}
}

if (!isInitialised) {
setOutput("Cannot format yet, as the formatter engine is being initialised. Try again soon.");
return;
}

setOutput("Formatting ...");
outputFormat();
}, [currentLanguage, idempotence, tolerateParsingErrors, isInitialised]);

// Init page (runs only once, but twice in strict mode in dev)
useEffect(() => {
const initWasm = async () => {
// Make sure we only run this once
if (initCalled.current) return;
initCalled.current = true;

await init();
await topiaryInit();
await init(); // Does the WebAssembly.instantiate()
await topiaryInit(); // Does the TreeSitter::init()
setIsInitialised(true);
}

Expand All @@ -78,10 +67,78 @@ function App() {
.catch(console.error);
}, []);

// EsLint react-hooks/exhaustive-deps:
// A 'runFormat' function would make the dependencies of the useEffect Hook
// below change on every render. To fix this, we wrap the definition of
// 'runFormat' in its own useCallback() Hook.
const runFormat = useCallback(() => {
if (!isInitialised) {
setOutput("Cannot format yet, as the formatter engine is being initialised. Try again soon.");
return;
}

if (isQueryCompiling.current) {
setOutput("Query is being compiled. Try again soon.");
return;
}

// This is how to run async within useEffect and useCallback.
// https://devtrium.com/posts/async-functions-useeffect
const outputFormat = async () => {
try {
if (queryChanged.current) {
isQueryCompiling.current = true;
setOutput("Compiling query ...");
await queryInit(query, currentLanguage);
queryChanged.current = false;
isQueryCompiling.current = false;
}

setOutput("Formatting ...");
setOutput(await format(input, idempotence, tolerateParsingErrors));
setProcessingTime(performance.now() - start);
} catch (e) {
queryChanged.current = false;
isQueryCompiling.current = false;
setOutput(String(e));
}
}

const start = performance.now();
outputFormat();
}, [currentLanguage, idempotence, isInitialised, tolerateParsingErrors, input, query]);

// Run on every (debounced) input change, as well as when isInitialised is set.
useEffect(() => {
if (!onTheFlyFormatting) return;
runFormat(debouncedInput, debouncedQuery);

// This is how to run async within useEffect and useCallback.
// https://devtrium.com/posts/async-functions-useeffect
const run = async () => {
await runFormat();
}

// We don't want to run whenever a dependency changes, but only when either of these do:
if (previousDebouncedInput.current !== debouncedInput ||
previousDebouncedQuery.current !== debouncedQuery ||
previousIsInitialised.current !== isInitialised) {
if (!isInitialised) {
setOutput("Cannot format yet, as the formatter engine is being initialised. Try again soon.");
return;
}

if (isQueryCompiling.current) {
setOutput("Query is being compiled. Try again soon.");
return;
}

run()
.catch(console.error);
}

previousDebouncedInput.current = debouncedInput;
previousDebouncedQuery.current = debouncedQuery;
previousIsInitialised.current = isInitialised;
}, [isInitialised, debouncedInput, debouncedQuery, onTheFlyFormatting, runFormat])

function changeLanguage(l: string) {
Expand All @@ -95,14 +152,15 @@ function App() {
if (!hasModification || window.confirm(confirmationMessage)) {
setInput(languages[l].input);
setQuery(languages[l].query);
queryChanged.current = true;
setOutput("");
setCurrentLanguage(l);
}
}
}

function handleFormat() {
runFormat(input, query);
runFormat();
};

function handleOnTheFlyFormatting() {
Expand Down Expand Up @@ -152,7 +210,7 @@ function App() {
<div className="columns">
<div className="column">
<h1>Query</h1>
<Editor id="query" value={query} onChange={s => setQuery(s)} placeholder="Enter your query here ..." />
<Editor id="query" value={query} onChange={s => { setQuery(s); queryChanged.current = true; }} placeholder="Enter your query here ..." />
</div>
<div className="column">
<h1>Input</h1>
Expand Down

0 comments on commit fc03b3f

Please sign in to comment.