Implementing Mouse Lock, conclusion
This post is the last in a series I’m writing about my work to implement the Mouse Lock API in Mozilla. I’m doing the work with students in my Mozilla Open Source course at Seneca College, and so these posts are intentionally didactic and self reflective. The aim of the series is to show how a new feature gets implemented in Firefox. Please note that all code discussed is unreviewed and not part of a shipping Firefox at this point.
We're done. Friday was the last day of classes, and I'm happy to report that we've finished our patch and got it up for review. We've also gotten 80% of the way on the tests, which I'll talk about below. I'll tell you honestly that I didn't think we'd get it completed by the end of the course, so it was all the sweeter to actually get to post it on the last day of class. Even better, for me, is the fact that we're done on time because of the hard work of quite a few students.
We've had a working Mouse Lock for many weeks now, but it took us a long time to iron out all the wrinkles in the spec (and we still have more). We managed to complete almost everything we set out to do. I'm still stumped on the best way to handle a few things, and am glad to be into review now, where I can get advice on implementing the final few bits.
Review is one of the most important parts of open source development. It's not about people criticizing you so much as helping you make your code the best it can be. I do expect to get lots of constructive criticism of our code, and also to fail our first review(s)--it will likely take 3-5 iterations on this patch before it's ready to land. In the meantime we'll get expert advice from people who really know the code where we've been working. If you can get over your own ego long enough to listen to the feedback, review can be a wonderful process.
Since my last post we've been working (and working fast) on so many separate bits of the code in parallel I'm having trouble remembering all of it. I'll focus on three things that stand out for me: 1) giving up control and running a mini-Mozilla on github; 2) the challenges of keeping a patch working against trunk; 3) the complexity of testing.
Many open source projects, Mozilla included, have sayings: "if you have to ask who owns it, you do;" or "if you find it you fix it;" and "no good deed goes unpunished." There's so much work to do on code this large that the only way things get done is if people take ownership of issues and--to borrow yet another phrase--"drive them into the tree." Without strong ownership, bugs quietly sit unnoticed, and issues persist. In order to teach my students how a feature gets into Mozilla, I decided to use these same approaches. It's the reason Matthew ended-up fixing bug 334573. It's the reason Raymond ended-up managing check-ins for all the students' tests.
[Side-note: I'd love Mouse Lock on github's Network View]
At a certain point it made more sense for me to give up control of the tree and let Raymond manage it. He'd been doing a great job merging my code with other students', so he obviously could be trusted with more responsibility. Open source isn't (or shouldn't be) about holding on to power. Rather, it's about finding opportunities to share that power with others. Going it alone doesn't scale. It's also the case the people end up in roles vs. seeking them out. You become the person who does X by doing X. It's often that simple. Running a mini-Mozilla project on github was interesting. I chose git and github simply because the students already knew it, and I didn't want to add Mercurial as yet another layer of complexity. By having a distributed version control system, it was easy to give the students the chance to take control of the entire process of getting this code written and managing the tree. It was very successful, and I'd do it again.
Speaking of version control, another aspect of adding a feature like Mouse Lock is that you have to keep a non-trivial patch working against trunk. The foundation on which you're doing your work is constantly shifting. For example, on the day we submitted our patch, we had to deal with changes to the navigator object, as well as changes to the fullscreen API, both of which broke our code. Obviously not every change to trunk is going to touch code where you're working. But if you're making changes to things that are in heavy traffic, you're going to get run over. A lot.
To keep the students from getting overwhelmed by this, I chose to do a weekly merge with mozilla-central. On any given day that's 50-100 checkins, many of which are big merges from feature branches. It adds up to thousands of changes every time we merged, and often conflicts, especially in our DOM code. However, this is what it's really like! Only in a few cases did we get into trouble with bad merges (I was to blame for one of the worst ones). I think it was a great lesson for the students. It also showed them why you want to get things done as soon as you can, and that understanding your tools is key to having your work progress.
The aspect of our work that took the longest this time was writing our tests. Tests are an important part of any new feature or bug fix in Mozilla, and without them, no well meaning feature is going to ship. I misjudged how complex the students would find writing Mouse Lock tests, assuming that having done as much JavaScript as we had, they'd just pick it up. I was wrong.
Mozilla has many types of automated tests, and the kind we need is Mochitest. A Mochitest is a web page with test assertions similar to jQuery's QUnit test framework. It also has ways to do things normally not allowed by a web page, including interacting with the browser's Chrome, and moving the mouse. Our tests would need to cover as much of the spec as we could. Writing tests is something of an art, since you're trying to produce the conditions for edge cases. As a group we came up with a list of tests that needed to get written, and then divided them up among students in the class. The students then wrote lots of tests. After writing their tests, I asked them to review and help each other fix issues in their code.
Almost none of the tests I saw would have been accepted in review, either for style issues, broken code, or timing bugs. The number one anti-pattern I saw in the tests was that people didn't understand the flow of asynchronous functions and events. The tests would be a mix of asynchronous and synchronous calls, and would fail in strange ways due to timing issues. I wasn't prepared for how hard they'd find this, and in future I'll spend more time teaching them these fundamentals. Here's a typical, simplified example:
... var pointer = navigator.pointer; // Test 1: Setup some code that should allow mouse lock to work ... pointer.lock( element, function() { ok(pointer.islocked(), "Pointer should be locked!"); } ); // Test 2: Setup some code that should force mouse lock to fail ... pointer.lock( element, function() { /* shouldn't happen */ }, function() { ok(!pointer.islocked(), "Pointer shouldn't be locked!"); } );
In the code above, the author rightly uses callbacks to check for success or failure, but then calls pointer.lock twice in a row, the second time before the success callback of the first has completed. The same kind of problem was evident with tests involving focus, blur, fullscreen (which nearly every one of our tests required!), the DOM being ready, etc.
Once we got these issues fixed, and the majority of the tests in place, we still had a problem, since every single one was failing when run as part of the full mochitest suite. This turned out to be a restriction on iframes and needing fullscreen mode. The fix, as with so many things, involved studying how others had faced similar issues. One good side effect to the issues we hit is that our tests seemed to have caught a crash bug in another part of the code.
As we were doing final tests I made try server builds so we could test on different machines without needing to build. Within a day of those builds being up, and me blogging about it, Brandon Jones had added Mouse Lock support to his amazing WebGL Quake 3 demo. I recorded a video of what it looks like, which you can see here:
It's rewarding to see people taking your work and doing such cool things with it so quickly. I don't think I've ever had a last class where I got to show something as cool as Quake 3 with Mouse Lock running in the browser and been able to say, "you did this."
While our work on this feature isn't 100% done, since reviews will inevitably mean more fixes and tests, this series is at an end. I got an email from one of the students today asking, "Sir, can I still work on this after the course is over?" The answer is obviously 'yes,' and if you too would like to continue following along, please add yourself to the bug.
I hope in this series I've managed to do a few things. First, to show you that it's not as hard as you think to contribute to a project as large as Firefox. Second, and at the same time, that while it is pretty hard to contribute to code this large, it isn't impossible. Getting things done involves patience, persistence, and the willingness to ask good questions when you get stuck. The Mozilla community wants to see you succeed, and is, for the most part, quite helpful.
Lastly, to professors and students reading this series, I'd encourage you to work on real code together. You don't have to be at the "right" school, to do work that changes the web, and you don't have to be experts. You can be a bunch of college kids in Toronto, and as long as you're hungry and willing to push yourselves, you can get there. The risks are part of what makes it so much fun. It doesn't have to be Mozilla, obviously. Any open source project would be glad to get a patch for something like Mouse Lock from you. Most of all, don't be afraid to fail, since that's exactly what it takes to win. I think we won.