Inside a fast CSS engine
hacks.mozilla.org
hacks.mozilla.org
Myself and my colleagues would/could write up a technical breakdown of something neat or innovative we might have done to solve some problem at work, but we sure as shit can't make cool little graphics interspersed between opportune paragraphs, nor could we figure out how to make the thing entertaining to read.
Is this kind of thing done in coordination with like a PR/graphics department?
I've been hired before by companies to take something their engineering team has done and turn it into a blogpost. I can't do graphics, but I can write a mean blogpost about pretty much anything you can think of.
Some posts/articles that came out of such initiatives have gone on to get published in dead-tree magazines or stick around the frontpage of HN for a few hours.
Broadly speaking, if you were doing this as a job job, I think it would fall under "developer evangelist". Your job is to make the company look good to potential hires and API consumers. Do whatever it takes. That involves going to conferences, writing blogposts, drawing graphics, and using freelancer resources to help you out in areas where you can't go. Usually evangelists can do most of the job on their own.
Her talk on react fiber this year is quite good as well: https://www.youtube.com/watch?v=ZCuYPiUIONs
I talked more about this on a recently recorded Hanselminutes podcast. It should come out soon.
Did you also do the React Fiber cartoons?
By the way, what program are you using to make your art?
IMO understanding how to appeal to non-tech-y users (and that tech-y users don't always want to sift through 5 pages of links to SDK-tarballs when trying to download a software) was how they managed to compete with IE/Microsoft back in the day. They're also one of the few non-profit organizations releasing software that understands user interfaces.
All that stuff going on with FF57 and whatnot must be tough for them but overall, I believe they're a huge success story and more open projects should look up to them for inspiration. If there was an operating system (sorry, Firefox OS didn't count) or office suite developed with a similar attitude, it could really make an impact.
Just compare the download page of Libre Office (https://www.libreoffice.org/download/download/) with that of Firefox. I'd link the latter, but it basically just starts to download the file you actually want. It's the little things...
Without the budget of Mozilla, there is no time to put enough thought and effort on presentation. Our volunteers are our money. I wear like five different hats in LibreOffice and do not get paid for any of them. If I focus on web layouts for some weeks, I watch in horror as our unconfirmed bug stats start growing out of control.
The number of people actively thinking about LibreOffice web presentation and layout is zero. If someone wants to start doing it for no compensation whatsoever, feel free to ping me.
Having said that, the grandparent reflects an undeniable truth of our time. User expectations are high, users are "spoiled": they expect it all and they expect it for free. Not commenting on whether that is a good or bad thing, just pointing out that it is true.
The sense of automatic entitlement, with no expectation of reciprocity or putting in the hard work (in open source or otherwise), appears pervasive.
It's a bit of a pity really, as it would be nice to take some of the insight of the not-quite-so-graphically-gifted engineers and have them collaborate with colleagues to present it in a great accessible format, but I've not seen this happen in practice.
And along the way, Mozilla created what is perhaps the most disruptive programming language of the past decade. For free. And open source.
It's really hard to appreciate the gravity of this.
Do you have a link? I went looking for information about embedding Servo and found nothing.
I can't seem to find the original example I'm thinking of that showed how to build an Android application (using cargo-apk) that embedded Servo.
Stylo and the like let us get improvements out to users well before we need to deal with that.
Basically we intend for it to work eventually on android, but it didn't get prioritized.
Stylo can be disabled at build time, and this was the case for 55 and is still the case for android. We don't remove the associated prefs in that case (about:config doesn't have UX because it's not exactly user facing).
You can go to about:support to see if stylo is actually enabled. There's a "stylo" column. It's not there in 55, but no 55 release has stylo enabled at build time; so if you don't have that column at all, stylo isn't there.
I remember the original Mozilla, how it started out as a total rewrite of Navigator. It took years to reach feature parity and Navigator lost all of its market share in that time.
Being able to do refactorings like this, and introduce a whole new language at the same time, is seriously impressive engineering. Bravo.
The article says We’re swapping in parts from our experimental browser Servo but servo.org says Servo is a modern high-performance browser engine. That's quite a difference.
Would be quite happy with servo being an engine and firefox being the browser (love firefox btw), just like how webkit/blink are the engines powering chrome/safari/opera. with bits and pieces of servo landing into firefox (just like how the CSS engine is landing in now).
I could be wrong though.
// Perform CSS selector matching and flow construction.
if traversal_driver.is_parallel() {
let pool = self.parallel_traversal.as_ref().unwrap();
// Parallel mode
parallel::traverse_dom::<ServoLayoutElement, RecalcStyleAndConstructFlows>(
&traversal, element, token, pool);
} else {
// Sequential mode
sequential::traverse_dom::<ServoLayoutElement, RecalcStyleAndConstructFlows>(
&traversal, element, token);
}parallel and sequential traverse_dom is generic traversal code which calls DomTraversal::process_preorder etc to do actual work. Here is lightly edited code:
impl<'a, E> DomTraversal<E> for RecalcStyleAndConstructFlows<'a>
where E: TElement,
E::ConcreteNode: LayoutNode,
E::FontMetricsProvider: Send,
{
fn process_preorder<F>(&self, traversal_data: &PerLevelTraversalData,
context: &mut StyleContext<E>, node: E::ConcreteNode,
note_child: F)
where F: FnMut(E::ConcreteNode)
{
if !node.is_text_node() {
let el = node.as_element().unwrap();
let mut data = el.mutate_data().unwrap();
recalc_style_at(self, traversal_data, context, el, &mut data, note_child);
}
}
}The hard parts, in this case, would be in Rust, most likely. I'm assuming that flipping CSS to parallel is not a trivial process. I'm further assuming that there is still some sort of locking mechanism so that you don't fire off two parallel traversals at the same time. Or allow updates during a scan. (Or abort running scans if an update happens?)
//! Implements parallel traversal over the DOM tree.
//!
//! This traversal is based on Rayon, and therefore its safety is largely
//! verified by the type system.
/// A parallel top-down DOM traversal.
///
/// This algorithm traverses the DOM in a breadth-first, top-down manner. The
/// goals are:
/// * Never process a child before its parent (since child style depends on
/// parent style). If this were to happen, the styling algorithm would panic.
/// * Prioritize discovering nodes as quickly as possible to maximize
/// opportunities for parallelism. But this needs to be weighed against
/// styling cousins on a single thread to improve sharing.
/// * Style all the children of a given node (i.e. all sibling nodes) on
/// a single thread (with an upper bound to handle nodes with an
/// abnormally large number of children). This is important because we use
/// a thread-local cache to share styles between siblings.I'm also curious if this pretty much prohibits ever having a parent selector. I guess you could do a first pass through the styles to remove any DAG nature from them?
For not-very-difficult details, read https://github.com/servo/servo/blob/master/components/select... searching for NotMatchedAndRestartFromClosestLaterSibling and HAS_SLOW_SELECTOR_LATER_SIBLINGS.
Siblings... I'm assuming there is nothing hard there. Positions, maybe?
I was also shooting from the hip for things I'm interested in, here. I'm not sure it matters. But, if on scanning, you pick relevant rules to apply based on the path to a node, then I could see some trickiness on having to consider both the top down and the bottom up paths to a node.
Siblings.., I don't think has this problem. Not sure why I thought it might.
And javascript is ultimately a different topic. So I wasn't worried about that here.
I missed your point about selector matching and style application. I think that is ultimately where my hip shot missed on this. (I have never had to reserve the right to be wrong. Just to assume it. :)
I still feel there is some danger there, but I think that is just clinging to an initial shot. I am curious why rust's help was needed to get this, now. A basic thread pool seems like it would have been somewhat easy to wire up in any language.
One neat thing is that we use rust-bindgen to walk C++ data structures, which extends Rust's concurrency guarantees into C++. We also have an FFI layer for invoking C++ code, and we have some careful static analysis of that callgraph from the entry points to be sure we're not mutating anything.
It's a very very odd mix of language that sounds like it's directed at a very young child and standard technical speak. Not the usual for the Hacks blog.
Not to fault the article too much, but I just found the tone a bit confusing. Even veering towards condescension in some parts, though I'm certain that's entirely accidental and wasn't the author's intent at all.
And personally, I loved the illustrations, as well.
I think Chrome tried something similar when they were introducing the chrome browser, although maybe with a different balance.
I don't think the tone here necessarily does anything for generating interest though. This is highly subjective, but at least for me it made for a jarring read and I lost interest pretty quickly. As I said above, I respect the intent behind the article, but I just found it very odd.
That technical background may not be as programmers. It may be power users, or web developers, or the like.
And I thought the analogies and illustrations helped out with giving some context that people who aren't systems developers might be missing.
1. The brief diversion about registers vs RAM and the brain analogy seemed a bit silly. I feel like anyone who would be interested in this article would find it completely unnecessary. It was just a tiny paragraph and one doodle though so it didn't really detract from the article.
2. Going the other way, Lin could have spent another few sentences talking about the compositing step and how GPUs enter into the picture (also why some CSS properties are "compositor-only"). I know compositing is tangential to the core of the article, but there were other brief tangents like the Rust stuff so a similar blurb about compositing wouldn't have been out of place (plus I find it super interesting).
Those are my only two nitpicks and the rest of the article did an excellent job threading the needle between informativeness and approachability without being too fluffy or verbose (e.g. Lin rightly didn't waste words explaining what the DOM is).
Also, excellent use of Rust.
I should hope so! It was one of the major motivators behind Mozilla's stewardship of Rust.
Is this really what happens under the hood?
1. If I overlap 52 html <div>s like a deck of cards, does the browser really paint all 52 div rectangles before compositing them?
2. If I overlap 52 <g>s like a deck of cards, does the browser really paint all 52 <g>s before compositing them?
3. In Qt, if I overlap 52 QML rectangles like a deck of cards, does the renderer only paint the parts of the rectangles that will be visible in the viewport? I was under the impression that this was the case, but I may be misunderstanding how the Qt QML scenegraph (or whatever it is called) works in practice.
edit: typo
Well, if they have alpha, it should.
Browsers don't typically do very good occlusion culling in general.
WebRender aims to change that, by using the hardware Z-buffer. :)
Not all GPUs have the Z-Buffer fillrate to make that approach viable(esp on mobile). I've seen more than a few cases where it was actually faster to turn off Z-Buffering and do the overdraw. More than a few architectures share Z-Buffer bandwidth with other pipelines.
On tiled architectures it'll also increase your tile count which can impact your per-drawcall overhead.
For low-tri things like UI you're better off doing the rect-culling yourself in software(ideally on another thread) and only falling back to the Z-Buffer if you have actual 3D transforms that need per-pixel culling.
Generally, we are so CPU bound that moving anything to the GPU is a win. We had to fight tooth and nail to make WebRender even 50% GPU bound...
I would argue though if overdraw vs z-buffer hurt your performance then you are more than 50% GPU bound :).
Uh, mobile GPUs tend to be way, way ahead of desktop GPUs in Z-Buffer bandwidth (especially in relative terms)
and you can't increase your tile count; there's a fixed number of tiles in the frame buffer (unless you're referring to not drawing some tiles at all - in which case set your scissor rect appropriately. oh, and if you can find out the GPU's tile size round your rectangle up to cover whole tiles)
The number of tiles can definitely change, it's something Qualcomm calls out directly in one of their talks[1]. As your tile count increases so does your setup cost for your drawcalls.
Nope, QML draws everything back-to-front with overdraw. From what I can tell, there is an ability to batch things into standard forward-renderer opaque/alpha passes, but that requires setting a flag QSGRenderNode::DepthAwareRendering which no builtin nodes set, from what I can tell, so the whole thing is skipped.
The core renderer code is here: http://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quick/s...
Search for m_useDepthBuffer and DepthAwareRendering and follow the trail -- the only use of the flag is from an example about raw OpenGL integration.
The rendering part of the question has been answered in another comment, but I would like to point out a couple things.
QML elements are `QObject`s with all the overhead[1] that comes with them. They are created even if they are not visible unless you have some `Loader` or C++ magic to prevent it. A QtWidget `QStyledItemDelegate` only had a single instance for all views/elements that used it. It could scale to millions of "cards" (assuming a QAbstractListModel was used) without any overhead[2].
So even if Qt manage to batch the draws and avoid painting everything, there is still a massive overhead in having such deck. I would suggest using a Loader and keeping only `n` cards loaded. If you want to minimize the performance impact.
[1] Object tree management, signal and slots, `n` connections for each QML expressions (recursively down to each tree leafs), memory impact, slower GC, etc.
[2] Assuming the model had the lazyloading functions implemented.
This is a fantastic technology and I feel like Servo has a pretty amazing future ahead of it. Exciting stuff.
emilio: pcwalton: wrt gecko we have stuff like https://bugzilla.mozilla.org/show_bug.cgi?id=1342220#c25 and similar
emilio: pcwalton: there's also the tp6 numbers, though those also measure CSS parsing and other stuff that isn't the style engine per se
pcwalton: emilio: our tp6 numbers are improved over Gecko at this point, yes? :)
emilio: pcwalton: amazon by a huge amount, facebook not yet I believe, but patches are on the queue that should make it turn around :)
emilio: pcwalton: happy to talk with mjs about impl details too, if he wants. I know a bit of WK stuff too :)
I'm mildly interested in impl details. But I'm more interested in what it speeds up and how it was measured (ideally in a form where I could try my own old-to-new and cross-browser comparisons, but just numbers would be interesting too).
We get significant speed ups from parallelism and such on big doms over all during page load, but we also get speedups from dynamic change handling implementing smarter invalidation than Gecko, which basically restyles the whole subtree / every sibling if it finds a relevant combinator affected by a change.
WebKit does much better than Gecko for class and other attribute changes, at least for descendants, where you go down with the relevant selectors. Stylo's system is relatively similar, but doing selector matching ltr, and handling the same way state and id changes.
You can see components/style/invalidation/element in the servo repo for the relevant code in that regard.
http://bholley.net/testcases/style-perf-tests/perf-reftest/b...
Firefox with STYLO_THREADS=1 gets about 160ms on my machine, which is basically parity with safari and chrome. With the parallelism, Firefox gets 40ms. :-)
You can also simulate sequential mode in recent nightlies by ctrl-clicking and loading the tab in the background (we disable parallelism for background loads).
For old numbers, you can get some in some of my old presentations at conferences. I think the LCA 2015 one[1] has the most details in that regard.
Is there any real life examples of achieved speedup?
Though, I have to admit I am also a little torn on this. Yes, browsing is typically done "during compile" or some other task. However, I have also begun doing most of that work remotely so that I can save battery on my laptop. To that end, it is now less of a concern on preserving cores for my tasks that actually need it.
And I seriously question whether my browser needs this to perform well. I am not completely closed to the idea, but I am highly skeptical.
Almost every modern device running a browser has multiple cores, and that trend is almost certainly going to increase – so it definitely feels like allowing a core part of the web platform to expand across cores will be beneficial.
It is also a bloat race. Web pages are getting increasingly complicated. With very little benefit to end users. I'd wager a growing number of the cycles and network requests are going to tracking, nowdays. Not to mention the ui language doing more and more contortions to give us a page that could be much more succinctly described.
None of this is to say I want it stopped. I just have a feeling of concern that this is leading to an ever increasing march to faster and faster machines to do basic work.
In the modern era, a program cannot take more than its fair share of CPU time - otherwise, a runaway program could easily render your computer nearly unusable. (Linux, macOS and Windows all use preemptive multitasking.)
The way to tell your operating system what you desire prioritized is, on *nix systems, 'nice'.
I'm not necessarily against all of this, but I'm also not eagerly embracing more crap to slow down my machine for no apparent reason.
I could almost guarantee you this is not the bottleneck of any modern setup.
Now, if the browser becomes more and more consuming, hitting my browser could get closer and closer to kicking off another giant compile.
And, as others, you are also assuming I was not pegging out my machine doing something by choice.
[0] https://youtu.be/7q9vIMXSTzc?t=35m (2015)
I want it to be true. I expect that someone should be able to show this with data. I've never seen it done, though.
Using the various CPU* options, you can turn on CPUAccounting, pin a given process and its children/threads to a range of CPUs, place CPUQuotas and so forth. There's a lot of power and granularity there.
I know, anecdotally, that devops/sysadmin folk use this to also audit and test energy consumption of processes over time. (Certain popular PID 1 programs have a run tool that allows you to easily, dynamically change and audit process resource usage.)
My typical use case, for instance, is auditing and managing various Emacs' processes lifetimes while running potentially racy elisp code.
It would be nice if everyone pushing some of these would collect some data for their claims, though. Especially if any of them have better setups (read: more than the single machine I have).
Stylo actually achieves nearly linear speedup if you just measure styling. It's really impressive.
Yes, most pages have significant speedups during the initial restyle especially. Wikipedia pages tend to get 3x or so improved style recalc time on typical systems with Intel quad core CPUs, for example.
Of course, styling is only one part of the whole, so your overall speedups are limited by the rest of the rendering pipeline. That's Amdahl's Law for you. But that's no reason to not parallelize at all; it just means that we have lots more work to do once this is done :)
Do you have absolute numbers? If it's 100 vs. 300 ms, that'd be huge. If it's 1 vs. 3 ms, I don't really care.
Note that Stylo is still faster than the old Gecko system even in sequential mode.
Does anyone know what it is about Firefox that makes the rest of my system unable to spawn new processes?