The Story of Stylo

Josh Matthews, @lastontheboat

The Story of Stylo

Josh Matthews (@lastontheboat)

Research Engineer, Mozilla

Firefox (Gecko)

9M lines of C/C++

Gecko's old style system

160K lines of C++

Gecko's new style system (Stylo)

85K lines of Rust

What is a style system?

A web browser is a pipeline.

What is a style system?

HTML parser:

What is a style system?

CSS parser:

What is a style system?

Cascade:

What is a style system?

Layout:

What is a style system?

The style system encompasses:

Why replace something that works?

Performance.

Cascading in parallel

This looks like an ideal parallel problem:

Awkward history

There have been two previous attempts to make the cascade parallel.

Both were abandoned.

Enter Servo

Completely new browser engine written in Rust.

Learning lessons from existing engines:

Enter Stylo

Project investigating integrating Servo's style system into Firefox.

Started in 2015; expanded in 2016; fully committed in 2017.

Integrating Stylo

Make use of Cargo dependencies, but vendor them in monorepo.

Continuous integration can't rely on external network.

Vendor entire Servo codebase in monorepo.

Integrating Stylo: Rust APIs

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.

Integrating Stylo: Rust APIs

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 {

Integrating Stylo: Rust APIs

C declaration for restyling DOM tree:

bool Servo_TraverseSubtree(
    RawGeckoElementBorrowed root,
    RawServoStyleSetBorrowed set,
    const mozilla::ServoElementSnapshotTable* snapshots,
    mozilla::ServoTraversalFlags flags);

Integrating Stylo: C++ APIs

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.

Integrating Stylo: C++ APIs

C API for creating an error reporter from Rust:

mozilla::css::ErrorReporter*
Gecko_CreateCSSErrorReporter(
    mozilla::ServoStyleSheet* sheet,
    mozilla::css::Loader* loader,
    nsIURI* uri);

Integrating Stylo: C++ APIs

Rust declaration for creating an error reporter:

pub extern "C" fn Gecko_CreateCSSErrorReporter(
    sheet: *mut ServoStyleSheet,
    loader: *mut Loader,
    uri: *mut nsIURI,
) -> *mut ErrorReporter;

Integrating Stylo: C++ APIs

Running bindgen during the build helps avoid mistakes.

It's also extremely slow and easy to trigger.

FFI choices

Two main choices for cross-language development:

Often turns into classic performance/safety trade-off.

FFI choices: Functions

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.

FFI choices: Functions

Calling into Gecko from Stylo:

unsafe {
    Gecko_SetImageElement(self, element.as_ptr());
}

FFI choices: Functions

Encapsulating complexity in Gecko:

void
Gecko_SetImageElement(nsStyleImage* aImage, nsIAtom* aAtom) {
  MOZ_ASSERT(aImage);
  aImage->SetElementId(do_AddRef(aAtom));
}

FFI choices: Types

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.

FFI choices: Types

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);

Idiomatic FFI from Rust

Instruct bindgen to convert specific types to more complex Rust types.

RawServoStyleSetOwned -> Owned<RawServoStyleSet>

C++:

typedef RawServoStyleSet* RawServoStyleSetOwned;

void Servo_StyleSet_Drop(RawServoStyleSetOwned set);

Idiomatic FFI from Rust

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>();
}

Idiomatic FFI from Rust

Instruct bindgen to convert specific types to more complex Rust types.

Parallel style system

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.

Parallel style system

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 challenges

Parallel code executes simultaneously; shared data can be a hazard.

Possibilities:

Rust prevents this problem at compile time.

Parallel style system hazards

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!

Parallel style system hazards

Very difficult to spot problems like this.

Simple rules for safety:

Let's get the compiler to help us!

Parallel style system hazards

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.

Pain points

Pain points: memory usage

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.

Pain points: memory usage

Enums are really great! Firefox developers love them!

Nested enums are always lurking, gobbling up your memory.

Pain points: memory usage

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

Pain points: memory usage

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

Pain points: memory usage

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

Pain points: memory usage

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.

Pain points: memory usage

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. 😞

Pain points: compile times

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.

Pain points: fallible allocation

Old Firefox style system relied on fine-grained control of allocations.

>80% of Firefox users are on Windows.

Substantial proportion use 32 bit builds.

Pain points: fallible allocation

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)

Anti-pain points

A conversation in #servo

<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

A conversation in #servo (cont.)

<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

A conversation in #servo (cont.)

<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

Thanks

Red panda (Firefox) Photo by Yortw