Josh Matthews, @lastontheboat
Research Engineer, Mozilla
A web browser is a pipeline.
HTML parser:
<html><head><style>...</style></head><body>...</body></html>
CSS parser:
span { color: red; }
div #header { color: green }
<span> | color: red |
<div id=header> | color: green |
Cascade:
<span>
color: red<div id=header>
color: green
Layout:
The style system encompasses:
window.getComputedStyle
)Performance.
This looks like an ideal parallel problem:
There have been two previous attempts to make the cascade parallel.
Both were abandoned.
Completely new browser engine written in Rust.
Learning lessons from existing engines:
Project investigating integrating Servo's style system into Firefox.
Started in 2015; expanded in 2016; fully committed in 2017.
Make use of Cargo dependencies, but vendor them in monorepo.
Continuous integration can't rely on external network.
Vendor entire Servo codebase in monorepo.
servo/ports/geckolib
contains crate with C APIs.
layout/style/ServoBindingList.h
has equivalent C function declarations.
All Servo APIs start with Servo_
.
Must be updated by hand when Rust-side APIs are modified.
Rust API for restyling DOM tree:
#[no_mangle]
pub extern "C" fn Servo_TraverseSubtree(
root: RawGeckoElementBorrowed,
raw_data: RawServoStyleSetBorrowed,
snapshots: *const ServoElementSnapshotTable,
raw_flags: ServoTraversalFlags
) -> bool {
C declaration for restyling DOM tree:
bool Servo_TraverseSubtree(
RawGeckoElementBorrowed root,
RawServoStyleSetBorrowed set,
const mozilla::ServoElementSnapshotTable* snapshots,
mozilla::ServoTraversalFlags flags);
layout/style/ServoBindings.h
has C APIs for calling from Rust.
All APIs start with Gecko_
.
Equivalent Rust FFI declarations generated by bindgen during build.
C API for creating an error reporter from Rust:
mozilla::css::ErrorReporter*
Gecko_CreateCSSErrorReporter(
mozilla::ServoStyleSheet* sheet,
mozilla::css::Loader* loader,
nsIURI* uri);
Rust declaration for creating an error reporter:
pub extern "C" fn Gecko_CreateCSSErrorReporter(
sheet: *mut ServoStyleSheet,
loader: *mut Loader,
uri: *mut nsIURI,
) -> *mut ErrorReporter;
Running bindgen during the build helps avoid mistakes.
It's also extremely slow and easy to trigger.
Two main choices for cross-language development:
Often turns into classic performance/safety trade-off.
Performance penalty every time we transition between C++ and Rust.
Compiler can't inline cross-language function calls.
Can assert invariants about values passed; good for safety.
Calling into Gecko from Stylo:
unsafe {
Gecko_SetImageElement(self, element.as_ptr());
}
Encapsulating complexity in Gecko:
void
Gecko_SetImageElement(nsStyleImage* aImage, nsIAtom* aAtom) {
MOZ_ASSERT(aImage);
aImage->SetElementId(do_AddRef(aAtom));
}
No performance penalty for directly reading/writing C values from Rust code.
Can violate invariants that were checked earlier.
Logic distributed between different pieces of code.
Setting up a Gecko gradient from Servo:
// NB: stops are guaranteed to be none in the gecko side
// by default.
let gecko_stop = unsafe {
&mut (*gecko_gradient).mStops[index]
};
gecko_stop.mColor = convert_rgba_to_nscolor(&stop.color);
Instruct bindgen to convert specific types to more complex Rust types.
RawServoStyleSetOwned
-> Owned<RawServoStyleSet>
typedef RawServoStyleSet* RawServoStyleSetOwned;
void Servo_StyleSet_Drop(RawServoStyleSetOwned set);
Instruct bindgen to convert specific types to more complex Rust types.
RawServoStyleSetOwned
-> Owned<RawServoStyleSet>
Rust:
type RawServoStyleSetOwned = Owned<RawServoStyleSet>;
extern fn Servo_StyleSet_Drop(data: RawServoStyleSetOwned) {
let _ = data.into_box::<PerDocumentStyleData>();
}
Instruct bindgen to convert specific types to more complex Rust types.
RawGeckoElementBorrowed
&RawGeckoElement
ServoStyleContextBorrowedOrNull
Option<&ServoStyleContext>
Hand-written back in 2013.
Switched to Rayon in 2016.
Made changes to Rayon to benefit Servo (breadth-first API).
Received benefits from unrelated Rayon changes.
Work queue of unstyled nodes; initially single root node.
Pool of worker threads.
When worker is free, take node from queue and style it.
When complete, add child nodes to work queue.
Parallel code executes simultaneously; shared data can be a hazard.
Possibilities:
Rust prevents this problem at compile time.
When re-entering C++, Rust can't help us any more.
Consider the following:
Element* nsIDocument::GetRootElement() {
return mCachedRootElement ?
mCachedRootElement : GetRootElementInternal();
}
This is not safe to invoke from worker threads simultaneously!
Very difficult to spot problems like this.
Simple rules for safety:
Let's get the compiler to help us!
sixgill - GCC plugin for analyzing C/C++.
Start at Gecko_
C APIs, follow all possible code paths.
If dangerous write found, report an error.
Over 30 pre-existing hazards detected; many new ones prevented.
Source code for static analysis.
Firefox is sensitive about its reputation for using memory.
C++ infrastructure for measuring heap allocations requires pointer to allocated buffer.
Some Rust types (e.g. HashMap) do not expose this pointer.
Losing coverage from the style system is really bad.
Enums are really great! Firefox developers love them!
Nested enums are always lurking, gobbling up your memory.
border-left-style: none | solid | dashed [spacing]
enum BorderStyle {
None, Solid, Dashed(u32)
}
Under the hood:
Tag | Data | Total | |
---|---|---|---|
BorderStyle | 4 bytes | 4 bytes | 8 bytes |
Let's use an Option<T> instead of an explicit None.
enum BorderStyleValue {
Solid, Dashed(u32)
}
type BorderStyle = Option<BorderStyleValue>;
Tag | Data | Total | |
---|---|---|---|
BorderStyleValue | 4 bytes | 4 bytes | 8 bytes |
Option<BorderStyleValue> | 4 bytes | 8 bytes | 12 bytes |
What if the dashed width is optional?
enum BorderStyleValue {
Solid, Dashed(Option<u32>)
}
type BorderStyle = Option<BorderStyleValue>;
Tag | Data | Total | |
---|---|---|---|
Option<u32> | 4 bytes | 4 bytes | 8 bytes |
BorderStyleValue | 4 bytes | 8 bytes | 12 bytes |
Option<BorderStyleValue> | 4 bytes | 12 bytes | 16 bytes |
We can flatten our enum hierarchy instead:
enum BorderStyle {
None, Solid, DashedNone, Dashed(u32)
}
Tag | Data | Total | |
---|---|---|---|
BorderStyle | 4 bytes | 4 bytes | 8 bytes |
Choosing between ergonomics and memory usage sucks.
One more instance - std::sync::Arc
supports weak pointers.
Stylo doesn't use weak pointers; that's an extra 4-8 bytes per allocation.
We chose to fork std::sync::Arc
. π
Unsurprisingly, compiling a huge amount of Rust code is slow.
That's ~300,000 lines of Rust code from the largest crate.
On 2015 Macbook: 1m 30s for debug; 6m 35s for release
Stylo requires stable Rust; we can't use incremental builds or ThinLTO yet.
Old Firefox style system relied on fine-grained control of allocations.
>80% of Firefox users are on Windows.
Substantial proportion use 32 bit builds.
There's an RFC that is still under discussion.
Standard library does not allow recovering from allocation failure.
πππππππππππππππππππππππππ
We forked std::collections::HashMap
and added fallible methods.
(also duplicated Vec::push
to add a try_push
method)
<heycam> one of the best parts about stylo has been how much easier it has been to implement these style system optimizations that we need, because Rust
<heycam> can you imagine if we needed to implement this all in C++ in the timeframe we have
<bholley> heycam: yeah srsly
<bholley> heycam: it's so rare that we get fuzz bugs in rust code
<bholley> heycam: considering all the complex stuff we're doing
* heycam remembers getting a bunch of fuzzer bugs from all kinds of style system stuff in gecko
<bholley> heycam: think about how much time we could save if each one of those annoying compiler errors today was swapped for a fuzz bug tomorrow :-)
<heycam> heh
<njn> you guys sound like an ad for Rust