CSC/ECE 517 Spring 2017/M1702 Implement the Mutation Observer API
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 handle 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.
Tracking issue
The Original Tracking Issue for the project can be found at the Tracking Issue page of the Mutation Observer Project.
Building the Project
Follow the Guidelines mentioned in the Read me file of the project Wiki Page for building the Project in a Linux Environment.
Project Description
- The changes made for the project are as per the initial steps given here.
Initial steps for Stub method Implementation:
- Forked the original (Servo)[1] repository into our GitHub account and created a new Branch (MutationObserver-dev)[2] for pushing our changes.
- Created the MutationObserver and MutationRecord interfaces with stub method implementations following the guidelines given here: Adding a new DOM Interface
- The IDL Files describe the Structure of the Mutation Observer and Mutation Record classes:
- Created the MutationObserver and MutationRecord interfaces with stub method implementations following the guidelines given here: Adding a new DOM Interface
MutationObserver IDL [Constructor(MutationCallback callback)] interface MutationObserver { void observe(Node target, optional MutationObserverInit options); void disconnect(); sequence<MutationRecord> takeRecords(); }; callback MutationCallback = void (sequence<MutationRecord> mutations, MutationObserver observer); dictionary MutationObserverInit { boolean childList = false; boolean attributes; boolean characterData; boolean subtree = false; boolean attributeOldValue; boolean characterDataOldValue; sequence<DOMString> attributeFilter; };
MutationRecord IDL [Exposed=Window] interface MutationRecord { readonly attribute DOMString type; [SameObject] readonly attribute Node target; [SameObject] readonly attribute NodeList addedNodes; [SameObject] readonly attribute NodeList removedNodes; readonly attribute Node? previousSibling; readonly attribute Node? nextSibling; readonly attribute DOMString? attributeName; readonly attribute DOMString? attributeNamespace; readonly attribute DOMString? oldValue; };
- Added the new interfaces in the dom scripts folder:
components/script/dom/mutationobserver.rs use core::ptr::null; use dom; use dom::bindings::codegen::Bindings::MutationObserverBinding::MutationCallback; use dom::bindings::codegen::Bindings::MutationObserverBinding::MutationObserverInit; use dom::bindings::error::Error; use dom::bindings::js::Root; use dom::bindings::reflector::Reflector; use dom::bindings::trace::JSTraceable; use dom::mutationrecord::MutationRecord; use dom::node::Node; use dom::window::Window; use dom_struct::dom_struct; use script_thread::ScriptThread; use std::ops::Deref; use std::rc::Rc; #[dom_struct] pub struct MutationObserver { reflector_: Reflector, callback: MutationCallback, } impl MutationObserver { fn new(global: &Window, callback: MutationCallback) -> MutationObserver { MutationObserver { reflector_: Reflector::new(), callback: callback, } } pub fn Constructor(global: &Window, callback: Rc<MutationCallback>) -> Result<Root<MutationObserver>, Error> { let observer = MutationObserver::new(global, Rc::deref(&callback)); ScriptThread::add_mutation_observer(&observer) } } components/script/dom/mutationrecord.rs use core::ptr::null; use dom_struct::dom_struct; use dom::bindings::codegen::Bindings::MutationRecordBinding::MutationRecordBinding::MutationRecordMethods; use dom::bindings::js::{JS, Root}; use dom::bindings::str::DOMString; use dom::bindings::reflector::Reflector; use dom::node::Node; use dom::nodelist::NodeList; use dom::window::Window; #[dom_struct] pub struct MutationRecord { reflector_: Reflector, // readonly attribute DOMString record_type; record_type: DOMString, // [SameObject] // readonly attribute Node target; target: Root<Node>, // [SameObject] // readonly attribute NodeList addedNodes; addedNodes: Root<NodeList>, // [SameObject] // readonly attribute NodeList removedNodes; removedNodes: Root<NodeList>, // readonly attribute Node? previousSibling; previousSibling: Root<Node>, // readonly attribute Node? nextSibling; nextSibling: Root<Node>, // readonly attribute DOMString? attributeName; attributeName: DOMString, // readonly attribute DOMString? attributeNamespace; attributeNamespace: DOMString, // readonly attribute DOMString? oldValue; oldValue: DOMString, } impl MutationRecord { fn new(window: &Window, record_type: DOMString, target: Root<Node>) -> Root<MutationRecord> { MutationRecord { reflector_: Reflector::new(), record_type: record_type, target: target, addedNodes: NodeList::empty(window), removedNodes: NodeList::empty(window), previousSibling: None, nextSibling: None, attributeName: None, attributeNamespace: None, oldValue: None, } } } impl MutationRecordMethods for MutationRecord { fn Type(&self) -> DOMString { return self.record_type; } fn Target(&self) -> Root<Node> { return self.target; } fn AddedNodes(&self) -> Root<NodeList> { return self.addedNodes; } fn RemovedNodes(&self) -> Root<NodeList> { return self.removedNodes; } fn GetPreviousSibling(&self) -> Option<Root<Node>> { if self.previousSibling.is_null() { return None; } else { return Some(self.previousSibling); } } fn GetNextSibling(&self) -> Option<Root<Node>> { if self.nextSibling.is_null() { return None; } else { return Some(self.nextSibling); } } fn GetAttributeName(&self) -> Option<DOMString> { if self.attributeName.is_null() { return None; } else { return Some(self.attributeName); } } fn GetAttributeNamespace(&self) -> Option<DOMString> { if self.attributeNamespace.is_null() { return None; } else { return Some(self.attributeNamespace); } } fn GetOldValue(&self) -> Option<DOMString> { if self.oldValue.is_null() { return None; } else { return Some(self.oldValue); } } }
- Added the new interfaces in the
mod.rs
file:
- Added the new interfaces in the
components/script/dom/mod.rs ... pub mod mutationobserver; pub mod mutationrecord; ...
- Added the new Interface bindings in the file:
components/script/dom/bindings/codegen/Bindings.conf ... DOMInterfaces = { ... 'MutationObserver': { 'nativeType': 'MutationObserver', 'path': 'dom::mutationobserver::MutationObserver', }, 'MutationRecord': { 'nativeType': 'MutationRecord', 'path': 'dom::mutationrecord::MutationRecord', }, ... } ...
- We hid the new interfaces by default by adding a
[Pref="dom.mutation_observer.enabled"]
attribute to bothmutationobserver.rs
andmutationrecord.rs
and added a corresponding preference toresources/prefs.json
file.
- We hid the new interfaces by default by adding a
resources/prefs.json ... "dom.mutation_observer.enabled": false, ...
- We added a vector of
MutationObserver
objects as a member of the ScriptThread as mentioed in the requirements.
components/script/script_thread.rs ... pub struct ScriptThread { ... /// a vector of MutationObserver objects mutation_observers: Vec<MutationObserver>, ... } ... impl ScriptThread { ... mutation_observers: vec![], ... } ...
- Implemented the
MutationObserver
constructor, by following the steps given in the DOM Specifications.
components/script/dom/mutationobserver.rs ... pub fn Constructor(global: &GlobalScope, callback: Rc<MutationCallback>) -> Result<Root<MutationObserver>, Error> { callback = &callback let mut frameBrowsingContext = Frames(&self) Ok(MutationObserver::new(global,this.callback)), Err(Error::new()) } ...
Testing
Since the tests were already present in the original Servo repository,Only the test expectation were needed to be updated. We did that as mentioned in the project description in the following steps:
- Added a
__dir__.ini
file totests/wpt/metadata/dom/nodes/
for enabling the new preference by including"prefs: ["dom.mutation_observer.enabled:true"]"
.
tests/wpt/metadata/dom/nodes/__dir__.ini prefs: ["dom.mutation_observer.enabled:true"]
- Tests are present in web-platform-tests directory (i.e
.tests/wpt/web-platform-tests/dom/nodes
). - 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 Patterns
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.
Conclusion
After making the above mentioned changes, we can create the Mutation Observer Object using the Constructor which we have implemented.
Subsequent Steps
After completing the above initial steps, we plan on performing the following tasks:
- add support for mutation observer microtasks
- add a mutation_observer_compound_microtask_queued member to ScriptThread
- implement the queue a mutation observer compound microtask algorithm by adding a new variant to the Microtask enum for mutation observers
- implement the notify mutation observers algorithm (ignoring the specific text about "execute a compound microtask") using the vector of observers previously added to ScriptThread
- add support for observing specific mutations
- add a vector of MutationObserver objects to Node
- implement MutationObserver.observe (step 7 and 8 mean "replace an existing matching entry with the new options, or add a new entry if there is no match")
- implement the queue a mutation record algorithm
- make changing/appending/removing/replacing an attribute queue a mutation record via Attr::set_value, Element::push_attribute, and Element::remove_first_matching_attribute
Commit Frequency
As per the guidelines of the Mozilla project, all the changes will be submitted in a single commit.
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/