The original address: keminglabs.com/blog/buildi…
keminglabs.com/
Release time: March 18, 2018
When I built Finda, I wanted it to be fast, especially in response to all user input in 16 milliseconds.
With this goal in mind, you might be surprised to learn that Finda is built with Electron, a framework that is often criticized for having the opposite speed.
In this article, I’ll explain how Finda uses Rust to take advantage of Electron’s advantages (ease of packaging, access to complex OPERATING system specific apis, browser visualization capabilities) while minimizing its drawbacks of unpredictable latency and memory usage.
Design considerations
Before diving into the technical details, let’s take a look at the design goals of Finda itself.
Finda supports a single interaction. You type things, and it finds things — browser tabs, text editor buffers, local files, browser history, open Windows, and so on.
D189ym6tlc5mr2.cloudfront.net/video/2018_…
The goal was to make Finda feel less like an application and more like command-tab, just part of the operating system itself, popping up immediately when needed and then disappearing quickly.
No menus, multiple Windows, buttons, or native UI of any kind are required. Really, Finda interaction only requires.
- A “global shortcut” to enhance Finda’s full screen, regardless of the current program in focus.
- Capture input method
- Rendering search results
Otherwise, Fender should be lurking backstage, making small talk.
Non-electron substitute
In view of these minimum requirements, I considered my options.
Native OS X: I decided against this option early on for two reasons.
1) I wanted to choose to port Finda to Windows and (I can’t believe I’m saying this) Linux because testers asked if they could buy a version for their respective platforms.
2) To develop natively with XCode, I would have to upgrade OS X, which would almost certainly destroy my computer in new and different ways than it currently does (which I have resolved peacefully).
Game-like: I’ve only written one pixel shader (gradient from white to black) in my life, but hey, the game is fast, maybe this will work? After exploring several options, I decided to try out a prototype using GGEz, a fantastic library of Rust games built on top of THE SDL.
I find the API approachable for graphics novices like myself (setting colors and drawing rectangles rather than pipes of structures that require byte shaders……) But I soon realized that these primitives still required a fair amount of work on my part to compile into a decent application.
For example, you can render text given a string, font size, and font. But Finda will highlight the match as you type.
Keminglabs.com/blog/buildi…
This means I need to deal with multiple fonts and colors and keep track of the bounding boxes of each drawn substring to layout everything
In addition to rendering, I also had trouble with operating system integration. I find it difficult.
- Create a “colorless” window (no title bar and minimize/maximize/close traffic light button).
- Run the application in the background without appearing in the Dock.
- “Global hotkeys” via the Quartz event service (After four hours, I managed to get the keycodes, but I gave up when I realized I needed to turn them into shortcuts through a separate set of circles to find live key graphs.
These aren’t really “gaming” issues, and a framework like GLUT (OpenGL) doesn’t seem any better than GEZ (SDL).
Electron: I’ve built applications with Electron before, and I know it will meet Finda’s requirements. Browsers were originally designed for typesetting text, and Electron offers a single-line API with extensive window options and global shortcuts.
The only unknown is performance, which is the subject of the rest of this article.
architecture
Tl for architecture; Dr Is, Electron is used as the user interface layer, and Rust binary handles everything else.
When Finda is opened and a key is pressed.
- The browser calls a Document onKeyDown listener, which translates the JavaScript keydown event into a plain JavaScript object representing the event; Similar to:
{name: "keydown", key: "C-g"}
Copy the code
- This JavaScript object is passed to Rust (more on that later), which returns another pure JavaScript object representing the state of the entire application.
{
query: "search terms",
results: [{label: "foo", icon: "bar.png"},... ] , selected_idx:2,
show_overlay: false. }Copy the code
3) Then pass this JavaScript object to react. js, which actually renders it into the DOM using
There are two things to note about this architecture.
First, Electron doesn’t maintain any one state — from its point of view, the entire application is a function of recent events. This is simply because the Rust aspect maintains Finda’s internal state.
Second, these steps occur in every user interaction (keyup and KeyDown). Therefore, in order to meet performance requirements, these three steps together must be completed in less than 16ms.
interoperability
The interesting part comes in step 2 — what does it look like to call Rust from JavaScript?
I’m building a Node.js module with Rust using the awesome Neon library.
From the Electron side, it feels just like calling any other type of package.
var Native = require("Native");
var new_app = Native.step({name: "keydown".key: "C-g"});
Copy the code
The Rust aspect of this function is a bit more complicated. Let’s do it in pieces.
pub fn step(call: call) - > JsResult < JsObject > {let scope = call. Scope. Reguments. The require (scope,0)?
let event = &call.arguments.require(scope, 0)? .check::<JsObject>()?let event_type.<JsObject>(); let event_type: String = event
.get(scope, "name")?
.downcast::<JsString>()
.unwrap()
.value();
Copy the code
JavaScript has several semantics that don’t map neatly to Rust’s language semantics (for example, parameter objects and the infamous this dynamic variable).
So instead of trying to map JS calls to Rust function signatures, Neon passes your function to a single Call object from which the details can be extracted. Since I’ve written the call (JS) aspect of this function, I know that the first and only argument will be a JavaScript object that always has a name key associated with the string value.
This event_type string can then be used to guide the rest of the “translation” from the JavaScript object to the appropriate variant of the Finda::Event enumeration.
match event_type.as_str() {
"blur" => finda::step(&mut app, finda::Event::Blur),
"hide" => finda::step(&mut app, finda::Event::Hide),
"show" => finda::step(&mut app, finda::Event::Show),
"keydown"= > {let s = event
.get(scope, "key")?
.downcast::<JsString>()
.unwrap()
.value();
finda::step(&mutapp, finda::Event::KeyDown(s)); }...Copy the code
Each of these branches calls the finda::step function, which actually updates the state of the application based on events — changing queries and returning relevant results, opening selected results, hiding Finda, and so on.
(I’ll write more details about Rust in a future blog post – if you want to be notified, sign up for my mailing list or follow @lynagHK).
After the application state is updated, it needs to be returned to the Electron port for rendering. The process looks similar, but in a different direction. Converting Rust data structures to JavaScript data structures:
let o = JsObject::new(scope);
o.set("show_overlay", JsBoolean::new(scope, app.show_overlay))? ; o.set("query", JsString::new(scope, &app.query).unwrap())? ; o.set("selected_idx",
JsNumber::new(scope, app.selected_idx as f64),
)?;
Copy the code
Here, we first create the JavaScript object that will be returned to Electron, and then associate the key with some primitive type.
Returning the result (an array of objects) requires a bit more detour. The size of the array must be declared beforehand and the enumeration Rust structure explicitly, but overall that’s fine.
let rs = JsArray::new(scope, app.results.len() as u32);
for (idx, r) in app.results.iter().enumerate() {
let jsr = JsObject::new(scope);
jsr.set("label", JsString::new(scope, &r.label).unwrap())? ;if let Some(ref icon) = r.icon {
jsr.set("icon", JsString::new(scope, &icon.pathname).unwrap())? ; } rs.set(idxas u32, jsr)? ; } o.set("results", rs)? ;Copy the code
Finally, at the end of the function, the JavaScript object is returned.
Ok(o)
Copy the code
Neon handles all the details, passing them on to the caller on the JavaScript side.
Performance verification
So how do all these machines perform in practice? The following is a typical single keystroke tracking, which can be seen in the “Performance” TAB of Chrome DevTools (built-in Electron).
Each step is labeled: 1) converting the key to an event, 2) handling the event in Rust, and 3) rendering the result with React.
The first thing to notice is the green bar at the top, which shows that all this happened within 14ms.
The second thing to look out for is oh dang Rust! Rust Interop (section 2, where the actual Native.step() call is highlighted in the Flamegraph) completes in less than a millisecond.
This particular keyDown event corresponds to adding a letter to my query, which means that within this millisecond Finda:
- For all my open Windows, Emacs buffers, about 20,000 page titles and urls in my browser history, and my
~/work/
,~/Downloads/
and~/Dropbox/
Search for all file names in the folder. - All of these results are sorted based on quality heuristic statistics (number of matches, occurrence at word boundaries, and so on).
- Translate the first 50 results into JavaScript and return them.
If you don’t believe me when I say it’s so fast, you can try downloading it yourself.
(Of course, my 2013 Macbook Air’s SSD isn’t fast enough to even list all of these files in a millisecond –Finda transparently builds and maintains an index. More on that in a future post.)
Most of the time was spent rendering. About 8 milliseconds for React (the render bar at the bottom of the FlameGraph) and 4 milliseconds for the layout of the browser itself (purple), drawing (green), and loading thumbnails from disk/cache (yellow on the far right).
The exact performance numbers vary from event to event, but this trace is typical. Rust takes a few milliseconds to do the “real work”, most of which is taken up by rendering, while the entire JavaScript execution is always completed in 16 milliseconds.
Reflect on performance
With these performance numbers in mind, one idea is to reduce response time by ditching React (and perhaps DOM altogether) and handling the layout and rendering manually with the
However, there are some serious diminishing returns. Regardless of whether humans can tell the difference between a 15ms response and a 5ms response, it’s likely that some low-level OS/graphics-driven /LCD physics governs the actual response time at this scale.
Another Angle is that we have an extra budget — we can “waste” that budget on a slower rendering path if that path has other benefits. And in the case of Electron, it does: In addition to the easy-to-use built-in profiling tools we’ve just seen, DOM and CSS offer wonderful runtime plasticity. Open the inspector and it’s easy to play with different fonts, colors, and spacing.
Keminglabs.com/blog/buildi…
For a completely data-driven application like Finda, having visual sketching and the ability to play in the production medium is crucial — it’s impossible to effectively prototype a search-based interaction by pushing pixels on a graphic design tool.
Maybe someone who knows OpenGL inside and out can do this sketching in Emacs, or maybe a game developer can put it together in Unity.
For me, I couldn’t imagine, prototype, and release Finda without Electron and Rust. They’re amazing technologies, so thank you very much to everyone who contributed to them.
comprehensive
Electron and Rust proved to fit well within Finda’s design limitations.
Electron makes it easy to build and publish desktop applications, freeing me from the tedious details of font rendering and low-level operating system hotkeys and window apis.
Rust makes it easy for me to write fast, secure, low-level data structures, and encourages me to think about memory and performance in a way that I usually ignore when I’m wearing a JavaScript/ClojureScript hat.
Together, they form a powerful combination. Try Finda and experience its speed for yourself.
If you try this Rust/Electron hybrid approach in your own projects, please drop me a line! If you try this Rust/Electron hybrid method in your own projects, please drop me a line! I’d love to hear about your work and help in any way I can.
Read more
-
Check out Dan Luu’s excellent work in measuring input delays in computers from the 1970s to today.
-
Check out Neon Github repo or @rustneon to learn how to build node.js extensions using Rust.
-
Xi Editor is an open source Rust text Editor with an emphasis on performance.
thank you
Thanks to Nikita Prokopov, Saul Pwanson, Tom Ballinger, Veit Heller, Julia Evans and Bert Muthalaly for their thoughtful feedback on this article.
Did you like this article? Subscribe to Kevin’s mailing list.
Translation via www.DeepL.com/Translator (free version)