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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
|
.. _apz:
Asynchronous Panning and Zooming
================================
**This document is a work in progress. Some information may be missing
or incomplete.**
.. image:: AsyncPanZoomArchitecture.png
Goals
-----
We need to be able to provide a visual response to user input with
minimal latency. In particular, on devices with touch input, content
must track the finger exactly while panning, or the user experience is
very poor. According to the UX team, 120ms is an acceptable latency
between user input and response.
Context and surrounding architecture
------------------------------------
The fundamental problem we are trying to solve with the Asynchronous
Panning and Zooming (APZ) code is that of responsiveness. By default,
web browsers operate in a “game loop” that looks like this:
::
while true:
process input
do computations
repaint content
display repainted content
In browsers the “do computation” step can be arbitrarily expensive
because it can involve running event handlers in web content. Therefore,
there can be an arbitrary delay between the input being received and the
on-screen display getting updated.
Responsiveness is always good, and with touch-based interaction it is
even more important than with mouse or keyboard input. In order to
ensure responsiveness, we split the “game loop” model of the browser
into a multithreaded variant which looks something like this:
::
Thread 1 (compositor thread)
while true:
receive input
send a copy of input to thread 2
adjust rendered content based on input
display adjusted rendered content
Thread 2 (main thread)
while true:
receive input from thread 1
do computations
rerender content
update the copy of rendered content in thread 1
This multithreaded model is called off-main-thread compositing (OMTC),
because the compositing (where the content is displayed on-screen)
happens on a separate thread from the main thread. Note that this is a
very very simplified model, but in this model the “adjust rendered
content based on input” is the primary function of the APZ code.
A couple of notes on APZ's relationship to other browser architecture
improvements:
1. Due to Electrolysis (e10s), Site Isolation (Fission), and GPU Process
isolation, the above two threads often actually run in different
processes. APZ is largely agnostic to this, as all communication
between the two threads for APZ purposes happens using an IPC layer
that abstracts over communication between threads vs. processes.
2. With the WebRender graphics backend, part of the rendering pipeline is
also offloaded from the main thread. In this architecture, the
information sent from the main thread consists of a display list, and
scrolling-related metadata referencing content in that display list.
The metadata is kept in a queue until the display list undergoes an
additional rendering step in the compositor (scene building). At this
point, we are ready to tell APZ about the new content and have it
start applying adjustments to it, as further rendering steps beyond
scene building are done synchronously on each composite.
The compositor in theory can continuously composite previously rendered
content (adjusted on each composite by APZ) to the screen while the
main thread is busy doing other things and rendering new content.
The APZ code takes the input events that are coming in from the hardware
and uses them to figure out what the user is trying to do (e.g. pan the
page, zoom in). It then expresses this user intention in the form of
translation and/or scale transformation matrices. These transformation
matrices are applied to the rendered content at composite time, so that
what the user sees on-screen reflects what they are trying to do as
closely as possible.
Technical overview
------------------
As per the heavily simplified model described above, the fundamental
purpose of the APZ code is to take input events and produce
transformation matrices. This section attempts to break that down and
identify the different problems that make this task non-trivial.
Checkerboarding
~~~~~~~~~~~~~~~
The area of page content for which a display list is built and sent to
the compositor is called the “displayport”. The APZ code is responsible
for determining how large the displayport should be. On the one hand, we
want the displayport to be as large as possible. At the very least it
needs to be larger than what is visible on-screen, because otherwise, as
soon as the user pans, there will be some unpainted area of the page
exposed. However, we cannot always set the displayport to be the entire
page, because the page can be arbitrarily long and this would require an
unbounded amount of memory to store. Therefore, a good displayport size
is one that is larger than the visible area but not so large that it is a
huge drain on memory. Because the displayport is usually smaller than the
whole page, it is always possible for the user to scroll so fast that
they end up in an area of the page outside the displayport. When this
happens, they see unpainted content; this is referred to as
“checkerboarding”, and we try to avoid it where possible.
There are many possible ways to determine what the displayport should be
in order to balance the tradeoffs involved (i.e. having one that is too
big is bad for memory usage, and having one that is too small results in
excessive checkerboarding). Ideally, the displayport should cover
exactly the area that we know the user will make visible. Although we
cannot know this for sure, we can use heuristics based on current
panning velocity and direction to ensure a reasonably-chosen displayport
area. This calculation is done in the APZ code, and a new desired
displayport is frequently sent to the main thread as the user is panning
around.
Multiple scrollable elements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Consider, for example, a scrollable page that contains an iframe which
itself is scrollable. The iframe can be scrolled independently of the
top-level page, and we would like both the page and the iframe to scroll
responsively. This means that we want independent asynchronous panning
for both the top-level page and the iframe. In addition to iframes,
elements that have the overflow:scroll CSS property set are also
scrollable. In the display list, scrollable elements are arranged in a
tree structure, and in the APZ code we have a matching tree of
AsyncPanZoomController (APZC) objects, one for each scrollable element.
To manage this tree of APZC instances, we have a single APZCTreeManager
object. Each APZC is relatively independent and handles the scrolling for
its associated scrollable element, but there are some cases in which they
need to interact; these cases are described in the sections below.
Hit detection
~~~~~~~~~~~~~
Consider again the case where we have a scrollable page that contains an
iframe which itself is scrollable. As described above, we will have two
APZC instances - one for the page and one for the iframe. When the user
puts their finger down on the screen and moves it, we need to do some
sort of hit detection in order to determine whether their finger is on
the iframe or on the top-level page. Based on where their finger lands,
the appropriate APZC instance needs to handle the input.
This hit detection is done by APZCTreeManager in collaboration with
WebRender, which has more detailed information about the structure of
the page content than is stored in APZ directly. See
:ref:`this section <wr-hit-test-details>` for more details.
Also note that for some types of input (e.g. when the user puts two
fingers down to do a pinch) we do not want the input to be “split”
across two different APZC instances. In the case of a pinch, for
example, we find a “common ancestor” APZC instance - one that is
zoomable and contains all of the touch input points, and direct the
input to that APZC instance.
Scroll Handoff
~~~~~~~~~~~~~~
Consider yet again the case where we have a scrollable page that
contains an iframe which itself is scrollable. Say the user scrolls the
iframe so that it reaches the bottom. If the user continues panning on
the iframe, the expectation is that the top-level page will start
scrolling. However, as discussed in the section on hit detection, the
APZC instance for the iframe is separate from the APZC instance for the
top-level page. Thus, we need the two APZC instances to communicate in
some way such that input events on the iframe result in scrolling on the
top-level page. This behaviour is referred to as “scroll handoff” (or
“fling handoff” in the case where analogous behaviour results from the
scrolling momentum of the page after the user has lifted their finger).
.. _input-event-untransformation:
Input event untransformation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The APZC architecture by definition results in two copies of a “scroll
position” for each scrollable element. There is the original copy on the
main thread that is accessible to web content and the layout and
painting code. And there is a second copy on the compositor side, which
is updated asynchronously based on user input, and corresponds to what
the user visually sees on the screen. Although these two copies may
diverge temporarily, they are reconciled periodically. In particular,
they diverge while the APZ code is performing an async pan or zoom
action on behalf of the user, and are reconciled when the APZ code
requests a repaint from the main thread.
Because of the way input events are represented, this has some
unfortunate consequences. Input event coordinates are represented
relative to the device screen - so if the user touches at the same
physical spot on the device, the same input events will be delivered
regardless of the content scroll position. When the main thread receives
a touch event, it combines that with the content scroll position in order
to figure out what DOM element the user touched. However, because we now
have two different scroll positions, this process may not work perfectly.
A concrete example follows:
Consider a device with screen size 600 pixels tall. On this device, a
user is viewing a document that is 1000 pixels tall, and that is
scrolled down by 200 pixels. That is, the vertical section of the
document from 200px to 800px is visible. Now, if the user touches a
point 100px from the top of the physical display, the hardware will
generate a touch event with y=100. This will get sent to the main
thread, which will add the scroll position (200) and get a
document-relative touch event with y=300. This new y-value will be used
in hit detection to figure out what the user touched. If the document
had a absolute-positioned div at y=300, then that would receive the
touch event.
Now let us add some async scrolling to this example. Say that the user
additionally scrolls the document by another 10 pixels asynchronously
(i.e. only on the compositor thread), and then does the same touch
event. The same input event is generated by the hardware, and as before,
the document will deliver the touch event to the div at y=300. However,
visually, the document is scrolled by an additional 10 pixels so this
outcome is wrong. What needs to happen is that the APZ code needs to
intercept the touch event and account for the 10 pixels of asynchronous
scroll. Therefore, the input event with y=100 gets converted to y=110 in
the APZ code before being passed on to the main thread. The main thread
then adds the scroll position it knows about and determines that the
user touched at a document-relative position of y=310.
Analogous input event transformations need to be done for horizontal
scrolling and zooming.
Content independently adjusting scrolling
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
As described above, there are two copies of the scroll position in the
APZ architecture - one on the main thread and one on the compositor
thread. Usually for architectures like this, there is a single “source
of truth” value and the other value is simply a copy. However, in this
case that is not easily possible to do. The reason is that both of these
values can be legitimately modified. On the compositor side, the input
events the user is triggering modify the scroll position, which is then
propagated to the main thread. However, on the main thread, web content
might be running Javascript code that programmatically sets the scroll
position (via window.scrollTo, for example). Scroll changes driven from
the main thread are just as legitimate and need to be propagated to the
compositor thread, so that the visual display updates in response.
Because the cross-thread messaging is asynchronous, reconciling the two
types of scroll changes is a tricky problem. Our design solves this
using various flags and generation counters. The general heuristic we
have is that content-driven scroll position changes (e.g. scrollTo from
JS) are never lost. For instance, if the user is doing an async scroll
with their finger and content does a scrollTo in the middle, then some
of the async scroll would occur before the “jump” and the rest after the
“jump”.
Content preventing default behaviour of input events
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Another problem that we need to deal with is that web content is allowed
to intercept touch events and prevent the “default behaviour” of
scrolling. This ability is defined in web standards and is
non-negotiable. Touch event listeners in web content are allowed call
preventDefault() on the touchstart or first touchmove event for a touch
point; doing this is supposed to “consume” the event and prevent
touch-based panning. As we saw in a previous section, the input event
needs to be untransformed by the APZ code before it can be delivered to
content. But, because of the preventDefault problem, we cannot fully
process the touch event in the APZ code until content has had a chance
to handle it.
To balance the needs of correctness (which calls for allowing web content
to successfully prevent default handling of events if it wishes to) and
responsiveness (which calls for avoiding blocking on web content
Javascript for a potentially-unbounded amount of time before reacting to
an event), APZ gives web content a "deadline" to process the event and
tell APZ whether preventDefault() was called on the event. The deadline
is 400ms from the time APZ receives the event on desktop, and 600ms on
mobile. If web content is able to process the event before this deadline,
the decision to preventDefault() the event or not will be respected. If
web content fails to process the event before the deadline, APZ assumes
preventDefault() will not be called and goes ahead and processes the
event.
To implement this, upon receiving a touch event, APZ immediately returns
an untransformed version that can be dispatched to content. It also
schedules the 400ms or 600ms timeout. There is an API that allows the
main-thread event dispatching code to notify APZ as to whether or not the
default action should be prevented. If the APZ content response timeout
expires, or if the main-thread event dispatching code notifies the APZ of
the preventDefault status, then the APZ continues with the processing of
the events (which may involve discarding the events).
To limit the responsiveness impact of this round-trip to content, APZ
tries to identify cases where it can rule out preventDefault() as a
possible outcome. To this end, the hit-testing information sent to the
compositor includes information about which regions of the page are
occupied by elements that have a touch event listener. If an event
targets an area outside of these regions, preventDefault() can be ruled
out, and the round-trip skipped.
Additionally, recent enhancements to web standards have given page
authors new tools that can further limit the responsiveness impact of
preventDefault():
1. Event listeners can be registered as "passive", which means they
are not allowed to call preventDefault(). Authors can use this flag
when writing listeners that only need to observe the events, not alter
their behaviour via preventDefault(). The presence of passive event
listeners does not cause APZ to perform the content round-trip.
2. If page authors wish to disable certain types of touch interactions
completely, they can use the ``touch-action`` CSS property from the
pointer-events spec to do so declaratively, instead of registering
event listeners that call preventDefault(). Touch-action flags are
also included in the hit-test information sent to the compositor, and
APZ uses this information to respect ``touch-action``. (Note that the
touch-action information sent to the compositor is not always 100%
accurate, and sometimes APZ needs to fall back on asking the main
thread for touch-action information, which again involves a
round-trip.)
Other event types
~~~~~~~~~~~~~~~~~
The above sections talk mostly about touch events, but over time APZ has
been extended to handle a variety of other event types, such as trackpad
and mousewheel scrolling, scrollbar thumb dragging, and keyboard
scrolling in some cases. Much of the above applies to these other event
types too (for example, wheel events can be prevent-defaulted as well).
Importantly, the "untransformation" described above needs to happen even
for event types which are not handled in APZ, such as mouse click events,
since async scrolling can still affect the correct targeting of such
events.
Technical details
-----------------
This section describes various pieces of the APZ code, and goes into
more specific detail on APIs and code than the previous sections. The
primary purpose of this section is to help people who plan on making
changes to the code, while also not going into so much detail that it
needs to be updated with every patch.
Overall flow of input events
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This section describes how input events flow through the APZ code.
Disclaimer: some details in this section are out of date (for example,
it assumes the case where the main thread and compositor thread are
in the same process, which is rarely the case these days, so in practice
e.g. steps 6 and 8 involve IPC, not just "stack unwinding").
1. Input events arrive from the hardware/widget code into the APZ via
APZCTreeManager::ReceiveInputEvent. The thread that invokes this is
called the "controller thread", and may or may not be the same as the
Gecko main thread.
2. Conceptually the first thing that the APZCTreeManager does is to
associate these events with “input blocks”. An input block is a set
of events that share certain properties, and generally are intended
to represent a single gesture. For example with touch events, all
events following a touchstart up to but not including the next
touchstart are in the same block. All of the events in a given block
will go to the same APZC instance and will either all be processed
or all be dropped.
3. Using the first event in the input block, the APZCTreeManager does a
hit-test to see which APZC it hits. If no APZC is hit, the events are
discarded and we jump to step 6. Otherwise, the input block is tagged
with the hit APZC as a tentative target and put into a global APZ
input queue. In addition the target APZC, the result of the hit test
also includes whether the input event landed on a "dispatch-to-content"
region. These are regions of the page where there is something going
on that requires dispatching the event to content and waiting for
a response _before_ processing the event in APZ; an example of this
is a region containing an element with a non-passive event listener,
as described above. (TODO: Add a section that talks about the other
uses of the dispatch-to-content mechanism.)
4.
i. If the input events landed outside a dispatch-to-content region,
any available events in the input block are processed. These may
trigger behaviours like scrolling or tap gestures.
ii. If the input events landed inside a dispatch-to-content region,
the events are left in the queue and a timeout is initiated. If
the timeout expires before step 9 is completed, the APZ assumes
the input block was not cancelled and the tentative target is
correct, and processes them as part of step 10.
5. The call stack unwinds back to APZCTreeManager::ReceiveInputEvent,
which does an in-place modification of the input event so that any
async transforms are removed.
6. The call stack unwinds back to the widget code that called
ReceiveInputEvent. This code now has the event in the coordinate
space Gecko is expecting, and so can dispatch it to the Gecko main
thread.
7. Gecko performs its own usual hit-testing and event dispatching for
the event. As part of this, it records whether any touch listeners
cancelled the input block by calling preventDefault(). It also
activates inactive scrollframes that were hit by the input events.
8. The call stack unwinds back to the widget code, which sends two
notifications to the APZ code on the controller thread. The first
notification is via APZCTreeManager::ContentReceivedInputBlock, and
informs the APZ whether the input block was cancelled. The second
notification is via APZCTreeManager::SetTargetAPZC, and informs the
APZ of the results of the Gecko hit-test during event dispatch. Note
that Gecko may report that the input event did not hit any
scrollable frame at all. The SetTargetAPZC notification happens only
once per input block, while the ContentReceivedInputBlock
notification may happen once per block, or multiple times per block,
depending on the input type.
9.
i. If the events were processed as part of step 4(i), the
notifications from step 8 are ignored and step 10 is skipped.
ii. If events were queued as part of step 4(ii), and steps 5-8
complete before the timeout, the arrival of both notifications
from step 8 will mark the input block ready for processing.
iii. If events were queued as part of step 4(ii), but steps 5-8 take
longer than the timeout, the notifications from step 8 will be
ignored and step 10 will already have happened.
10. If events were queued as part of step 4(ii) they are now either
processed (if the input block was not cancelled and Gecko detected a
scrollframe under the input event, or if the timeout expired) or
dropped (all other cases). Note that the APZC that processes the
events may be different at this step than the tentative target from
step 3, depending on the SetTargetAPZC notification. Processing the
events may trigger behaviours like scrolling or tap gestures.
If the CSS touch-action property is enabled, the above steps are
modified as follows:
* In step 4, the APZC also requires the allowed touch-action behaviours
for the input event. This might have been determined as part of the
hit-test in APZCTreeManager; if not, the events are queued.
* In step 6, the widget code determines the content element at the point
under the input element, and notifies the APZ code of the allowed
touch-action behaviours. This notification is sent via a call to
APZCTreeManager::SetAllowedTouchBehavior on the input thread.
* In step 9(ii), the input block will only be marked ready for processing
once all three notifications arrive.
Threading considerations
^^^^^^^^^^^^^^^^^^^^^^^^
The bulk of the input processing in the APZ code happens on what we call
“the controller thread”. In practice the controller thread could be the
Gecko main thread, the compositor thread, or some other thread. There are
obvious downsides to using the Gecko main thread - that is,“asynchronous”
panning and zooming is not really asynchronous as input events can only
be processed while Gecko is idle. In an e10s environment, using the Gecko
main thread of the chrome process is acceptable, because the code running
in that process is more controllable and well-behaved than arbitrary web
content. Using the compositor thread as the controller thread could work
on some platforms, but may be inefficient on others. For example, on
Android (Fennec) we receive input events from the system on a dedicated
UI thread. We would have to redispatch the input events to the compositor
thread if we wanted to the input thread to be the same as the compositor
thread. This introduces a potential for higher latency, particularly if
the compositor does any blocking operations - blocking SwapBuffers
operations, for example. As a result, the APZ code itself does not assume
that the controller thread will be the same as the Gecko main thread or
the compositor thread.
Active vs. inactive scrollframes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The number of scrollframes on a page is potentially unbounded. However,
we do not want to create a separate displayport for each scrollframe
right away, as this would require large amounts of memory. Therefore,
scrollframes as designated as either “active” or “inactive”. Active
scrollframes get a displayport, and an APZC on the compositor side.
Inactive scrollframes do not get a displayport (a display list is only
built for their viewport, i.e. what is currently visible) and do not get
an APZC.
Consider a page with a scrollframe that is initially inactive. This
scroll frame does not get an APZC, and therefore events targeting it will
target the APZC for the nearest active scrollable ancestor (let's call it
P; note, the rootmost scroll frame in a given process is always active).
However, the presence of the inactive scroll frame is reflected by a
dispatch-to-content region that prevents events over the frame from
erroneously scrolling P.
When the user starts interacting with that content, the hit-test in the
APZ code hits the dispatch-to-content region of P. The input block
therefore has a tentative target of P when it goes into step 4(ii) in the
flow above. When gecko processes the input event, it must detect the
inactive scrollframe and activate it, as part of step 7. Finally, the
widget code sends the SetTargetAPZC notification in step 8 to notify the
APZ that the input block should really apply to this new APZC. An issue
here is that the transaction containing metadata for the newly active
scroll frame must reach the compositor and APZ before the SetTargetAPZC
notification. If this does not occur within the 400ms timeout, the APZ
code will be unable to update the tentative target, and will continue to
use P for that input block. Input blocks that start after the transaction
will get correctly routed to the new scroll frame as there will now be an
APZC instance for the active scrollframe.
This model implies that when the user initially attempts to scroll an
inactive scrollframe, it may end up scrolling an ancestor scrollframe.
Only after the round-trip to the gecko thread is complete is there an
APZC for async scrolling to actually occur on the scrollframe itself. At
that point the scrollframe will start receiving new input blocks and will
scroll normally.
Note: with Fission (where inactive scroll frames would make it impossible
to target the correct process in all situations; see
:ref:`this section <fission-hit-testing>` for more details) and WebRender
(which makes displayports more lightweight as the actual rendering is
offloaded to the compositor and can be done on demand), inactive scroll
frames are being phased out, and we are moving towards a model where all
scroll frames with nonempty scroll ranges are active and get a
displayport and an APZC. To conserve memory, displayports for scroll
frames which have not been recently scrolled are kept to a "minimal" size
equal to the viewport size.
WebRender Integration
~~~~~~~~~~~~~~~~~~~~~
This section describes how APZ interacts with the WebRender graphics
backend.
Note that APZ predates WebRender, having initially been written to work
with the earlier Layers graphics backend. The design of Layers has
influenced APZ significantly, and this still shows in some places in the
code. Now that the Layers backend has been removed, there may be
opportunities to streamline the interaction between APZ and WebRender.
HitTestingTree
^^^^^^^^^^^^^^
The APZCTreeManager keeps as part of its internal state a tree of
HitTestingTreeNode instances. This is referred to as the HitTestingTree.
The main purpose of the HitTestingTree is to model the spatial
relationships between content that's affected by async scrolling. Tree
nodes fall roughly into the following categories:
* Nodes representing scrollable content in an active scroll frame. These
nodes are associated with the scroll frame's APZC.
* Nodes representing other content that may move in special ways in
response to async scrolling, such as fixed content, sticky content, and
scrollbars.
* (Non-leaf) nodes which do not represent any content, just metadata
(e.g. a transform) that applies to its descendant nodes.
An APZC may be associated with multiple nodes, if e.g. a scroll frame
scrolls two pieces of content that are interleaved with non-scrolling
content.
Arranging these nodes in a tree allows modelling relationships such as
what content is scrolled by a given scroll frame, what the scroll handoff
relationships are between APZCs, and what content is subject to what
transforms.
An additional use of the HitTestingTree is to allow APZ to keep content
processes up to date about enclosing transforms that they are subject to.
See :ref:`this section <sending-transforms-to-content-processes>` for
more details.
(In the past, with the Layers backend, the HitTestingTree was also used
for compositor hit testing, hence the name. This is no longer the case,
and there may be opportunities to simplify the tree as a result.)
The HitTestingTree is created from another tree data structure called
WebRenderScrollData. The relevant types here are:
* WebRenderScrollData which stores the entire tree.
* WebRenderLayerScrollData, which represents a single "layer" of content,
i.e. a group of display items that move together when scrolling (or
metadata applying to a subtree of such layers). In the Layers backend,
such content would be rendered into a single texture which could then
be moved asynchronously at composite time. Since a layer of content can
be scrolled by multiple (nested) scroll frames, a
WebRenderLayerScrollData may contain scroll metadata for more than one
scroll frame.
* WebRenderScrollDataWrapper, which wraps WebRenderLayerScrollData
but "expanded" in a way that each node only stores metadata for
a single scroll frame. WebRenderScrollDataWrapper nodes have a
1:1 correspondence with HitTestingTreeNodes.
It's not clear whether the distinction between WebRenderLayerScrollData
and WebRenderScrollDataWrapper is still useful in a WebRender-only world.
The code could potentially be revised such that we directly build and
store nodes of a single type with the behaviour of
WebRenderScrollDataWrapper.
The WebRenderScrollData structure is built on the main thread, and
then shipped over IPC to the compositor where it's used to construct
the HitTestingTree.
WebRenderScrollData is built in WebRenderCommandBuilder, during the
same traversal of the Gecko display list that is used to build the
WebRender display list. As of this writing, the architecture for this is
that, as we walk the Gecko display list, we query it to see if it
contains any information that APZ might need to know (e.g. CSS
transforms) via a call to ``nsDisplayItem::UpdateScrollData(nullptr,
nullptr)``. If this call returns true, we create a
WebRenderLayerScrollData instance for the item, and populate it with the
necessary information in ``WebRenderLayerScrollData::Initialize``. We also
create WebRenderLayerScrollData instances if we detect (via ASR changes)
that we are now processing a Gecko display item that is in a different
scrollframe than the previous item.
The main sources of complexity in this code come from:
1. Ensuring the ScrollMetadata instances end on the proper
WebRenderLayerScrollData instances (such that every path from a leaf
WebRenderLayerScrollData node to the root has a consistent ordering of
scrollframes without duplications).
2. The deferred-transform optimization that is described in more detail
at the declaration of ``StackingContextHelper::mDeferredTransformItem``.
.. _wr-hit-test-details:
Hit-testing
^^^^^^^^^^^
Since the HitTestingTree is not used for actual hit-testing purposes
with the WebRender backend (see previous section), this section describes
how hit-testing actually works with WebRender.
The Gecko display list contains display items
(``nsDisplayCompositorHitTestInfo``) that store hit-testing state. These
items implement the ``CreateWebRenderCommands`` method and generate a "hit-test
item" into the WebRender display list. This is basically just a rectangle
item in the WebRender display list that is no-op for painting purposes,
but contains information that should be returned by the hit-test (specifically
the hit info flags and the scrollId of the enclosing scrollframe). The
hit-test item gets clipped and transformed in the same way that all the other
items in the WebRender display list do, via clip chains and enclosing
reference frame/stacking context items.
When WebRender needs to do a hit-test, it goes through its display list,
taking into account the current clips and transforms, adjusted for the
most recent async scroll/zoom, and determines which hit-test item(s) are under
the target point, and returns those items. APZ can then take the frontmost
item from that list (or skip over it if it happens to be inside a OOP
subdocument that's ``pointer-events:none``) and use that as the hit target.
Note that the hit-test uses the last transform provided by the
``SampleForWebRender`` API (see next section) which generally reflects the
last composite, and doesn't take into account further changes to the
transforms that have occurred since then. In practice, we should be
compositing frequently enough that this doesn't matter much.
When debugging hit-test issues, it is often useful to apply the patches
on bug 1656260, which introduce a guid on Gecko display items and propagate
it all the way through to where APZ gets the hit-test result. This allows
answering the question "which nsDisplayCompositorHitTestInfo was responsible
for this hit-test result?" which is often a very good first step in
solving the bug. From there, one can determine if there was some other
display item in front that should have generated a
nsDisplayCompositorHitTestInfo but didn't, or if display item itself had
incorrect information. The second patch on that bug further allows exposing
hand-written debug info to the APZ code, so that the WR hit-testing
mechanism itself can be more effectively debugged, in case there is a problem
with the WR display items getting improperly transformed or clipped.
The information returned by WebRender to APZ in response to the hit test
is enough for APZ to identify a HitTestingTreeNode as the target of the
event. APZ can then take actions such as scrolling the target node's
associated APZC, or other appropriate actions (e.g. initiating a scrollbar
drag if a scrollbar thumb node was targeted by a mouse-down event).
Sampling
^^^^^^^^
The compositing step needs to read the latest async transforms from APZ
in order to ensure scrollframes are rendered at the right position. The API for this is
exposed via the ``APZSampler`` class. When WebRender is ready to do a composite,
it invokes ``APZSampler::SampleForWebRender``. In here, APZ gathers all async
transforms that WebRender needs to know about, including transforms to apply
to scrolled content, fixed and sticky content, and scrollbar thumbs.
Along with sampling the APZ transforms, the compositor also triggers APZ
animations to advance to the next timestep (usually the next vsync). This
happens just before reading the APZ transforms.
Fission Integration
~~~~~~~~~~~~~~~~~~~
This section describes how APZ interacts with the Fission (Site Isolation)
project.
Introduction
^^^^^^^^^^^^
Fission is an architectural change motivated by security considerations,
where web content from each origin is isolated in its own process. Since
a page can contain a mixture of content from different origins (for
example, the top level page can be content from origin A, and it can
contain an iframe with content from origin B), that means that rendering
and interacting with a page can now involve coordination between APZ and
multiple content processes.
.. _fission-hit-testing:
Content Process Selection for Input Events
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Input events are initially received in the browser's parent process.
With Fission, the browser needs to decide which of possibly several
content processes an event is targeting.
Since process boundaries correspond to iframe (subdocument) boundaries,
and every (html) document has a root scroll frame, process boundaries are
therefore also scroll frame boundaries. Since APZ already needs a hit
test mechanism to be able to determine which scroll frame an event
targets, this hit test mechanism was a good fit to also use to determine
which content process an event targets.
APZ's hit test was therefore expanded to serve this purpose as well. This
mostly required only minor modifications, such as making sure that APZ
knows about the root scroll frames of iframes even if they're not
scrollable. Since APZ already needs to process all input events to
potentially apply :ref:`untransformations <input-event-untransformation>`
related to async scrolling, as part of this process it now also labels
input events with information identifying which content process they
target.
Hit Testing Accuracy
^^^^^^^^^^^^^^^^^^^^
Prior to Fission, APZ's hit test could afford to be somewhat inaccurate,
as it could fall back on the dispatch-to-content mechanism to wait for
a more accurate answer from the main thread if necessary, suffering a
performance cost only (not a correctness cost).
With Fission, an inaccurate compositor hit test now implies a correctness
cost, as there is no cross-process main-thread fallback mechanism.
(Such a mechanism was considered, but judged to require too much
complexity and IPC traffic to be worth it.)
Luckily, with WebRender the compositor has much more detailed information
available to use for hit testing than it did with Layers. For example,
the compositor can perform accurate hit testing even in the presence of
irregular shapes such as rounded corners.
APZ leverages WebRender's more accurate hit testing ability to aim to
accurately select the target process (and target scroll frame) for an
event in general.
One consequence of this is that the dispatch-to-content mechanism is now
used less often than before (its primary remaining use is handling
`preventDefault()`).
.. _sending-transforms-to-content-processes:
Sending Transforms To Content Processes
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Content processes sometimes need to be able to convert between screen
coordinates and their local coordinates. To do this, they need to know
about any transforms that their containing iframe and its ancestors are
subject to, including async transforms (particularly in cases where the
async transforms persist for more than just a few frames).
APZ has information about these transforms in its HitTestingTree. With
Fission, APZ periodically sends content processes information about these
transforms so that they are kept relatively up to date.
Testing
-------
APZ makes use of several test frameworks to verify the expected behavior
is seen.
Mochitest
~~~~~~~~~
The APZ specific mochitests are useful when specific gestures or events need to be tested
with specific content. The APZ mochitests are located in `gfx/layers/apz/test/mochitest`_.
To run all of the APZ mochitests, run something like the following:
::
./mach mochitest ./gfx/layers/apz/test/mochitest
The APZ mochitests are often organized as subtests that run in a group. For example,
the `test_group_hittest-2.html`_ contains >20 subtests like
`helper_hittest_overscroll.html`_. When working on a specific subtest, it is often
helpful to use the `apz.subtest` preference to filter the subtests run to just the
tests you are working on. For example, the following would only run the
`helper_hittest_overscroll.html`_ subtest of the `test_group_hittest-2.html`_ group.
::
./mach mochitest --setpref apz.subtest=helper_hittest_overscroll.html \
./gfx/layers/apz/test/mochitest/test_group_hittest-2.html
For more information on mochitest, see the `Mochitest Documentation`_.
.. _gfx/layers/apz/test/mochitest: https://searchfox.org/mozilla-central/source/gfx/layers/apz/test/mochitest
.. _test_group_hittest-2.html: https://searchfox.org/mozilla-central/source/gfx/layers/apz/test/mochitest/test_group_hittest-2.html
.. _helper_hittest_overscroll.html: https://searchfox.org/mozilla-central/source/gfx/layers/apz/test/mochitest/helper_hittest_overscroll.html
.. _Mochitest Documentation: /testing/mochitest-plain/index.html
GTest
~~~~~
The APZ specific GTests can be found in `gfx/layers/apz/test/gtest/`_. To run
these tests, run something like the following:
::
./mach gtest "APZ*"
For more information, see the `GTest Documentation`_.
.. _GTest Documentation: /gtest/index.html
.. _gfx/layers/apz/test/gtest/: https://searchfox.org/mozilla-central/source/gfx/layers/apz/test/gtest/
Reftests
~~~~~~~~
The APZ reftests can be found in `layout/reftests/async-scrolling/`_ and
`gfx/layers/apz/test/reftest`_. To run the relevant reftests for APZ, run
a large portion of the APZ reftests, run something like the following:
::
./mach reftest ./layout/reftests/async-scrolling/
Useful information about the reftests can be found in the `Reftest Documentation`_.
There is no defined process for choosing which directory the APZ reftests
should be placed in, but in general reftests should exist where other
similar tests do.
.. _layout/reftests/async-scrolling/: https://searchfox.org/mozilla-central/source/layout/reftests/async-scrolling/
.. _gfx/layers/apz/test/reftest: https://searchfox.org/mozilla-central/source/gfx/layers/apz/test/reftest/
.. _Reftest Documentation: /layout/Reftest.html
Threading / Locking Overview
----------------------------
Threads
~~~~~~~
There are three threads relevant to APZ: the **controller thread**,
the **updater thread**, and the **sampler thread**. This table lists
which threads play these roles on each platform / configuration:
===================== ============= ============== =============
APZ Thread Name Desktop Desktop+GPU Android
===================== ============= ============== =============
**controller thread** UI main GPU main Java UI
**updater thread** SceneBuilder SceneBuilder SceneBuilder
**sampler thread** RenderBackend RenderBackend RenderBackend
===================== ============= ============== =============
Locks
~~~~~
There are also a number of locks used in APZ code:
======================= ==============================
Lock type How many instances
======================= ==============================
APZ tree lock one per APZCTreeManager
APZC map lock one per APZCTreeManager
APZC instance lock one per AsyncPanZoomController
APZ test lock one per APZCTreeManager
Checkerboard event lock one per AsyncPanZoomController
======================= ==============================
Thread / Lock Ordering
~~~~~~~~~~~~~~~~~~~~~~
To avoid deadlocks, the threads and locks have a global **ordering**
which must be respected.
Respecting the ordering means the following:
- Let "A < B" denote that A occurs earlier than B in the ordering
- Thread T may only acquire lock L, if T < L
- A thread may only acquire lock L2 while holding lock L1, if L1 < L2
- A thread may only block on a response from another thread T while holding a lock L, if L < T
**The lock ordering is as follows**:
1. UI main
2. GPU main (only if GPU process enabled)
3. Compositor thread
4. SceneBuilder thread
5. **APZ tree lock**
6. RenderBackend thread
7. **APZC map lock**
8. **APZC instance lock**
9. **APZ test lock**
10. **Checkerboard event lock**
Example workflows
^^^^^^^^^^^^^^^^^
Here are some example APZ workflows. Observe how they all obey
the global thread/lock ordering. Feel free to add others:
- **Input handling** (with GPU process): UI main -> GPU main -> APZ tree lock -> RenderBackend thread
- **Sync messages** in ``PCompositorBridge.ipdl``: UI main thread -> Compositor thread
- **GetAPZTestData**: Compositor thread -> SceneBuilder thread -> test lock
- **Scene swap**: SceneBuilder thread -> APZ tree lock -> RenderBackend thread
- **Updating hit-testing tree**: SceneBuilder thread -> APZ tree lock -> APZC instance lock
- **Updating APZC map**: SceneBuilder thread -> APZ tree lock -> APZC map lock
- **Sampling and animation deferred tasks** [1]_: RenderBackend thread -> APZC map lock -> APZC instance lock
- **Advancing animations**: RenderBackend thread -> APZC instance lock
.. [1] It looks like there are two deferred tasks that actually need the tree lock,
``AsyncPanZoomController::HandleSmoothScrollOverscroll`` and
``AsyncPanZoomController::HandleFlingOverscroll``. We should be able to rewrite
these to use the map lock instead of the tree lock.
This will allow us to continue running the deferred tasks on the sampler
thread rather than having to bounce them to another thread.
|