CSC/ECE 517 Spring 2017/M1702 Implement the Mutation Observer API Design
Mutation Observer API Project with SERVO & RUST
Servo is a prototype web browser engine written in the RUST language. The DOM standard defines a MutationObserver API that allows web content to receive callbacks when the page contents are mutated. In simple words, MutationObserver facilitates the developers with a technique to react to the changes in DOM. MutationObserver is designed to replace the Mutation Events defined in the DOM3 Events specification. The goal of this project is to implement the fundamental pieces required to support the Mutation Observer API.
Introduction
Servo
Servo is an open source web browser engine designed for application and embedded use. It is a high performance browser engine created by Mozilla Research and is written in the Rust language. The main idea behind the Servo is to create a parallel environment where the components are handled by isolated and fine grained tasks. These components can be rendering, HTML parsing etc.
Rust
Rust is an open source systems programming language developed by Mozilla. Servo is written in Rust. The main purpose behind it's design is to be thread safe and concurrent. The emphasis is also on speed, safety and control of memory layout.
Setting up and Building the Project
Follow the Guidelines mentioned in the read me page of the project's Wiki Page for building the Project in a Linux Environment.
Project Description
The purpose of this project is to implement the basic functionality required to implement a Mutation Observer API as defined in the DOM Specifications. In the First Phase of the Project, we implemented the initial Steps as described in the Project Specification. The initial implementation is merged in the original Servo Repository and the code can be found here. For the next phase of the project, we have to implement the functionality for supporting Mutation Observers. Based on the Project description, we have made a Project Design, which is described in detail below.
Project Design
High Level Design
Our Project Implements the MutationObserver API in the Mozilla's Servo Browser Engine. The High Level Design for the same is described below:
This is a high-level design of what the MutationObserver API actually does.
- According to the DOM Standard, when the contents of the page are mutated, the web content receives callback through the MutationObserver API.
- The changes are recorded by the MutationObserver using MutationRecords.
- The observe method of the MutationObserver is invoked when the contents of the page are mutated.
The technical design describes how we plan on implementing it.
Technical Design
Add support for mutation observer microtasks
- We will add a
mutation_observer_compound_microtask_queued
member to theScriptThread
class. This will be used later to implement the algorithm for queuing a mutation observer compound microtask and notifying a mutation observer as described below.
- Then, we will implement the queue a mutation observer compound microtask algorithm by adding a new variant to the Microtask enum for mutation observers. The algorithm for doing that is described below:
1) If the mutation observer compound microtask queued flag is set, then return. 2) Set the mutation observer compound microtask queued flag. 3) Queue a compound microtask which will be used to notify the mutation observers.
- Next, we will implement the notify mutation observers algorithm using the vector of observers previously added to the
ScriptThread
class. The notify mutation observers algorithm is implemented as described below:
1) Unset the mutation observer compound microtask queued flag, which was set in the previous algorithm. 2) Declare notifyList to be a copy of unit of related similar-origin browsing contexts' list of MutationObserver objects. 3) Declare signalList to be a copy of unit of related similar-origin browsing contexts' signal slot list. 4) Empty the unit of related similar-origin browsing contexts' signal slot list. 5) For each MutationObserver object mo in notifyList, we shall execute a compound microtask subtask to run these steps: 1) Declare queue to contain mo's record queue. 2) Empty mo's record queue. 3) All transient registered observers whose observer is mo will be removed. 4) If queue is non-empty, then invoke mo’s callback with a list of arguments consisting ofqueue
and mo, and mo as the callback this value. If this throws an exception, then we will report the exception. 6) For each slot in signalList, an event named slotchange will be fired in the same order as it is stored in thesignalist
, with its bubbles attribute set to true, at slot.
Add support for observing specific mutations
- Firstly we will add a vector of
MutationObserver
objects toNode
to store the Mutation Observers. - Then we will implement the observe method of MutationObserver. As per the DOM specification, the observe method has the following behaviour:
1) If either options’ attributeOldValue or attributeFilter is present and options’ attributes is omitted, then we set the options’ attributes to true. 2) If options’ characterDataOldValue is present and options’ characterData is omitted, then we set options’ characterData to true. 3) If none of options’ childList, attributes, and characterData is true, then we throw a TypeError. 4) If options’ attributeOldValue is true and options’ attributes is false, then we throw a TypeError. 5) If options’ attributeFilter is present and options’ attributes is false, then we throw a TypeError. 6) If options’ characterDataOldValue is true and options’ characterData is false, then we throw a TypeError. 7) For each registered observers "registered" in target’s list of registered observers whose observer is the context object we perform the following steps: 1) We remove all transient registered observers whose source is "registered". 2) We also replace registered’s options with options. 8) Otherwise, we add a new registered observers to target’s list of registered observers with the context object as the observer and options as the options, and then also, we add target to context object’s list of node on which it is registered.
Note: Steps 7 and 8 in the above description means that we replace an existing matching entry with the new options, or add a new entry if there is no match.
- Next, we will implement the queue a mutation record algorithm. Which is described as follows:
1) Declare the interested observers initially as an empty set of MutationObserver objects optionally paired with a string. 2) Declare nodes be the inclusive ancestors of target. 3) For each node in nodes, and then for each registered observer (with registered observer’s options as options) in node’s list of registered observers we decide: 1) If none of the following are true 1) node is not target and options’ subtree is false 2) type is "attributes" and options’ attributes is not true 3) type is "attributes", options’ attributeFilter is present, and options’ attributeFilter does not contain name or namespace is non-null 4) type is "characterData" and options’ characterData is not true 5) type is "childList" and options’ childList is false then,: 1) If registered observer’s observer is not in interested observers, we append registered observer’s observer to interested observers. 2) If either type is "attributes" and options’ attributeOldValue is true, or type is "characterData" and options’ characterDataOldValue is true,we set the paired string of registered observer’s observer in interested observers to oldValue. 4) For each observer in interested observers: 1) Declare record to be a new MutationRecord object with its type set to type and target set to target. 2) If name and namespace are given, we set record’s attributeName to name, and record’s attributeNamespace to namespace. 3) If addedNodes is given, we set record’s addedNodes to addedNodes. 4) If removedNodes is given, we set record’s removedNodes to removedNodes, 5) If previousSibling is given, we set record’s previousSibling to previousSibling. 6) If nextSibling is given, we set record’s nextSibling to nextSibling. 7) If observer has a paired string, we set record’s oldValue to observer’s paired string. 8) Then, append record to observer’s record queue. 5) Queue a mutation observer compound microtask.
- We will make changing/appending/removing/replacing of an attribute, queue a mutation record via the Attr::set_value, Element::push_attribute, and Element::remove_first_matching_attribute attributes. These changes are performed in the following steps:
1) Queue a mutation record of "attributes" for element with name attribute’s local name, namespace attribute’s namespace, and oldValue attribute’s value. 2) If element is custom, we enqueue a custom element callback reaction with element, callback name "attributeChangedCallback", and an argument list containing attribute’s local name, attribute’s value, value, and attribute’s namespace. 3) We then run theattribute change steps with element, attribute’s local name, attribute’s value, value, and attribute’s namespace. 4) We set attribute’s value to value.
1) Wequeue a mutation record of "attributes" for element with name attribute’slocal name, namespace attribute’s namespace, and oldValue null. 2) If element is custom, we enqueue a custom element callback reaction with element, callback name "attributeChangedCallback", and an argument list containing attribute’s local name, null, attribute’s value, and attribute’s namespace. 3) We then run the attribute change steps with element, attribute’s local name, null, attribute’s value, and attribute’s namespace. 4) We also append attribute to element’s attribute list. 5) We then set attribute’s element to element.
1) We queue a mutation record of "attributes" for element with name attribute’s local name, namespace attribute’s namespace, and oldValue attribute’s value. 2) If element is custom, we then enqueue a custom element callback reaction with element, callback name "attributeChangedCallback", and an argument list containing attribute’s local name, attribute’s value, null, and attribute’s namespace. 3) We run the attribute change steps with element, attribute’s local name, attribute’s value, null, and attribute’s namespace. 4) We then remove attribute from element’s attribute list. 5) We set attribute’s element to null.
1) Wequeue a mutation record of "attributes" for element with name oldAttr’s local name,namespace oldAttr’s namespace, and oldValue
oldAttr’s value.
2) If element is custom, we then enqueue a custom element callback reaction with element, callback name "attributeChangedCallback", and an
argument list containing oldAttr’s local name, oldAttr’s value, newAttr’s value, and oldAttr’s namespace.
3) We run the attribute change steps with element, oldAttr’s local name, oldAttr’s value, newAttr’s value, and oldAttr’s namespace.
4) We then replace oldAttr by newAttr in element’s attribute list.
5) We set oldAttr’s element to null.
6) We set newAttr’s element to element.
Test Plan
In the First Phase of the Project, our tests were written with regard to attributes, Childlist, Character data and records of a Mutation Observer. These tests are present in web-platform-tests directory (i.e .tests/wpt/web-platform-tests/dom/nodes
).
The project owner from Mozilla has mentioned that our existing tests will provide enough coverage for the newly added code as well. Furthermore, it was advised to create simple tests using the following steps:
1) The simplest way to create a new test is to use the following command: ./mach create-wpt tests/wpt/path/to/new/test.html. 2) This will create test.html in the appropriate directory using the WPT template for JavaScript tests. Tests are written using testharness.js. 3) To create a new reference test instead, use the following: ./mach create-wpt --reftest tests/wpt/path/to/new/reftest.html --reference tests/wpt/path/to/reference.html 4) reference.html will be created if it does not exist, and reftest.html will be created using the WPT reftest template. 5) These new tests can then be run in the following manner like any other WPT test: ./mach test-wpt tests/wpt/path/to/new/test.html and ./mach test-wpt tests/wpt/path/to/new/reftest.html
For this part of the project, our test plan is to check the following:
Testing the micro task for mutation observer.
Testing the observation of specific mutations:
Running the Tests
The web-platform tests can be run by giving the following command at the root directory of the project:
./mach test-wpt tests/wpt/web-platform-tests/dom/nodes.
We have Updated the corresponding test expectations as per the given guidelines in the tests/wpt/metadata/dom/nodes/
directory.
Design Pattern
Our Project follows the Observer Pattern, where a list of the objects dependent on the current object is maintained and the dependents are notified of any state changes. The Mutation Observer helps in achieving this functionality.
References
1. https://doc.rust-lang.org/stable/book/
2. https://en.wikipedia.org/wiki/Rust_(programming_language)
3. https://en.wikipedia.org/wiki/Servo_(layout_engine)
4. https://github.com/servo/servo
5. https://github.com/servo/servo/wiki/Mutation-observer-project
6. https://dom.spec.whatwg.org/