summaryrefslogtreecommitdiffstats
path: root/servo/components/style/parallel.rs
blob: 0e1c509ab7b8a8e18ae55a6458b70240480389f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

//! Implements parallel traversal over the DOM tree.
//!
//! This traversal is based on Rayon, and therefore its safety is largely
//! verified by the type system.
//!
//! The primary trickiness and fine print for the above relates to the
//! thread safety of the DOM nodes themselves. Accessing a DOM element
//! concurrently on multiple threads is actually mostly "safe", since all
//! the mutable state is protected by an AtomicRefCell, and so we'll
//! generally panic if something goes wrong. Still, we try to to enforce our
//! thread invariants at compile time whenever possible. As such, TNode and
//! TElement are not Send, so ordinary style system code cannot accidentally
//! share them with other threads. In the parallel traversal, we explicitly
//! invoke |unsafe { SendNode::new(n) }| to put nodes in containers that may
//! be sent to other threads. This occurs in only a handful of places and is
//! easy to grep for. At the time of this writing, there is no other unsafe
//! code in the parallel traversal.

#![deny(missing_docs)]

use crate::context::{StyleContext, ThreadLocalStyleContext};
use crate::dom::{OpaqueNode, SendNode, TElement};
use crate::scoped_tls::ScopedTLS;
use crate::traversal::{DomTraversal, PerLevelTraversalData};
use std::collections::VecDeque;

/// The minimum stack size for a thread in the styling pool, in kilobytes.
pub const STYLE_THREAD_STACK_SIZE_KB: usize = 256;

/// The stack margin. If we get this deep in the stack, we will skip recursive
/// optimizations to ensure that there is sufficient room for non-recursive work.
///
/// We allocate large safety margins because certain OS calls can use very large
/// amounts of stack space [1]. Reserving a larger-than-necessary stack costs us
/// address space, but if we keep our safety margin big, we will generally avoid
/// committing those extra pages, and only use them in edge cases that would
/// otherwise cause crashes.
///
/// When measured with 128KB stacks and 40KB margin, we could support 53
/// levels of recursion before the limiter kicks in, on x86_64-Linux [2]. When
/// we doubled the stack size, we added it all to the safety margin, so we should
/// be able to get the same amount of recursion.
///
/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1395708#c15
/// [2] See Gecko bug 1376883 for more discussion on the measurements.
pub const STACK_SAFETY_MARGIN_KB: usize = 168;

/// A callback to create our thread local context.  This needs to be
/// out of line so we don't allocate stack space for the entire struct
/// in the caller.
#[inline(never)]
pub(crate) fn create_thread_local_context<'scope, E>(slot: &mut Option<ThreadLocalStyleContext<E>>)
where
    E: TElement + 'scope,
{
    *slot = Some(ThreadLocalStyleContext::new());
}

// Sends one chunk of work to the thread-pool.
fn distribute_one_chunk<'a, 'scope, E, D>(
    items: VecDeque<SendNode<E::ConcreteNode>>,
    traversal_root: OpaqueNode,
    work_unit_max: usize,
    traversal_data: PerLevelTraversalData,
    scope: &'a rayon::ScopeFifo<'scope>,
    traversal: &'scope D,
    tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>,
) where
    E: TElement + 'scope,
    D: DomTraversal<E>,
{
    scope.spawn_fifo(move |scope| {
        gecko_profiler_label!(Layout, StyleComputation);
        let mut tlc = tls.ensure(create_thread_local_context);
        let mut context = StyleContext {
            shared: traversal.shared_context(),
            thread_local: &mut *tlc,
        };
        style_trees(
            &mut context,
            items,
            traversal_root,
            work_unit_max,
            traversal_data,
            Some(scope),
            traversal,
            tls,
        );
    })
}

/// Distributes all items into the thread pool, in `work_unit_max` chunks.
fn distribute_work<'a, 'scope, E, D>(
    mut items: VecDeque<SendNode<E::ConcreteNode>>,
    traversal_root: OpaqueNode,
    work_unit_max: usize,
    traversal_data: PerLevelTraversalData,
    scope: &'a rayon::ScopeFifo<'scope>,
    traversal: &'scope D,
    tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>,
) where
    E: TElement + 'scope,
    D: DomTraversal<E>,
{
    while items.len() > work_unit_max {
        let rest = items.split_off(work_unit_max);
        distribute_one_chunk(
            items,
            traversal_root,
            work_unit_max,
            traversal_data,
            scope,
            traversal,
            tls,
        );
        items = rest;
    }
    distribute_one_chunk(
        items,
        traversal_root,
        work_unit_max,
        traversal_data,
        scope,
        traversal,
        tls,
    );
}

/// Processes `discovered` items, possibly spawning work in other threads as needed.
#[inline]
pub fn style_trees<'a, 'scope, E, D>(
    context: &mut StyleContext<E>,
    mut discovered: VecDeque<SendNode<E::ConcreteNode>>,
    traversal_root: OpaqueNode,
    work_unit_max: usize,
    mut traversal_data: PerLevelTraversalData,
    scope: Option<&'a rayon::ScopeFifo<'scope>>,
    traversal: &'scope D,
    tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>,
) where
    E: TElement + 'scope,
    D: DomTraversal<E>,
{
    let local_queue_size = if tls.current_thread_index() == 0 {
        static_prefs::pref!("layout.css.stylo-local-work-queue.in-main-thread")
    } else {
        static_prefs::pref!("layout.css.stylo-local-work-queue.in-worker")
    } as usize;

    let mut nodes_remaining_at_current_depth = discovered.len();
    while let Some(node) = discovered.pop_front() {
        let mut children_to_process = 0isize;
        traversal.process_preorder(&traversal_data, context, *node, |n| {
            children_to_process += 1;
            discovered.push_back(unsafe { SendNode::new(n) });
        });

        traversal.handle_postorder_traversal(context, traversal_root, *node, children_to_process);

        nodes_remaining_at_current_depth -= 1;

        // If we have enough children at the next depth in the DOM, spawn them to a different job
        // relatively soon, while keeping always at least `local_queue_size` worth of work for
        // ourselves.
        let discovered_children = discovered.len() - nodes_remaining_at_current_depth;
        if discovered_children >= work_unit_max &&
            discovered.len() >= local_queue_size + work_unit_max &&
            scope.is_some()
        {
            let kept_work = std::cmp::max(nodes_remaining_at_current_depth, local_queue_size);
            let mut traversal_data_copy = traversal_data.clone();
            traversal_data_copy.current_dom_depth += 1;
            distribute_work(
                discovered.split_off(kept_work),
                traversal_root,
                work_unit_max,
                traversal_data_copy,
                scope.unwrap(),
                traversal,
                tls,
            );
        }

        if nodes_remaining_at_current_depth == 0 {
            traversal_data.current_dom_depth += 1;
            nodes_remaining_at_current_depth = discovered.len();
        }
    }
}