[ Music ]
[ Applause ]
Welcome to optimizing web content in your app.
My name's Jonathan Davis.
I'm the web technology's evangelist for Safari and WebKit.
Now let me get something out of the way up-front.
I've had people tell me I look like Edward Snowden, but I promise you I am not him, but with all the satellites flying overhead, they may be all out to get us.
That aside, I'm really excited to show you some new things that will help you get more performance out of web content in your app.
And we've all known for a long time that performance is key to a great user experience.
So if you're an app developer that uses WebViews and JSContext in app today.
And you care about performance, you're in the right place.
So we're talking about performance.
And in this day and age, what we really mean by performance is battery life.
I mean battery life is the ultimate limited resource.
It can be the difference in making that one last phone call or sending that last critical file before the battery dies.
And performance really matters to our users, and they naturally choose apps that don't slow them down and don't drain their battery.
So what's we've all learned together, from users' feedback and from each other, is that performance really matters because battery life matters.
So this year we focused on giving you better tools to find and fix performance issues in your web content.
Now we have tools for apps like for Swift, and Objective-C code, like Instruments.
And we have Web Inspector for web developers, creating web pages and sites.
But don't think that just because you choose to use web technologies in your app that you don't have any tools.
In fact, I'm going to show you today that all the same tools that are developed to help people build websites work just as well for web technologies in apps.
So I want to start today by showing you how to connect Web Inspector to your WebViews in JSContext, so you have the right tool ready to go when you need it.
And if you've ever wished you could see how memory is used by your web content, we've added new timelines to Web Inspector that I can't wait to show you.
They're going to save you so much time finding memory growth issues.
So let's get started.
And the first thing we're going to need to do is connect Web Inspector to our app content.
Now there are all sorts of reasons to use web technologies in your apps.
And some of you may be using JSContext with TVML in a tvOS app.
And another reason to use web technologies is when you want to show web content.
Like a web page from a third-party website.
A site you don't control.
And for that, you're probably using Safari View Controller and if you're not, you may want to take a look at last year's session Introducing Safari View Controller.
WKWebView is the best choice.
And it's essentially a rectangle where web content is drawn into your app.
It was introduced in iOS 8 and OS 10 Yosemite.
And if you're still using a WebView or UIWebView, you're really going to want to take a look at upgrading to WKWebView.
So if you want to learn more about WKWebView, I recommend taking a look at the 2014 talk that introduced the modern WebKit API.
So last year we added some great new features to WKWebView, like load file URL, custom user agent strings and the WK website data store API.
And today, with iOS 10 and macOS Sierra, we've improved 3D-touch support.
And now we allow your app to implement those sweet Peek and Pop events in WKWebView.
Now like I said earlier, just because you've chosen to use web technologies in your app, doesn't mean you don't have any tools.
But before you can use Web Inspector, you'll need to enable the develop menu.
So just load up Safari Preferences and go over to the Advanced tab and at the bottom, you'll see this check box.
And it says Show Develop menu in menu bar.
Just give that a click to check the box and the Develop Menu will appear in the menu bar for Safari.
Now to allow Web Inspector to connect your iOS devices, there's a setting you need to turn on.
So in the Settings app, on iOS, tap on Safari.
Then down at the bottom, tap on Advanced and toggle the Web Inspector setting On.
Now you can connect your device to your Mac and check out the Develop menu in Safari.
Now something that's really cool in the Develop menu, something you may not have ever noticed before, is you can see a list of devices attached.
There's an iPhone connected, a MacBook Pro and the simulators there.
And all you have to do to attach to one of these and start using the tools for debugging is just choose the Device menu.
And you get a list of all the WebViews and JSContexts that are running on the device.
And this Mac app here doesn't even use WebKit.
And I can connect right to it and use the tools.
Now for iOS, apps will only show up when you build and run them from Xcode.
But when we're talking about a Mac app, there's just one more thing you got to do.
To protect the integrity of your app, we don't let just anyone download your app and use Web Inspector to poke around your app.
So you'll need to add this entitlement to your app's Entitlements File for local development.
You probably already have an Entitlements file but if you don't, it's pretty easy to create one.
You just create a new plist with a .entitlements extension, set the code signing entitlements path to that file in Xcode's build settings.
So you add this while you're developing and then you take it back out when you ship your app.
Then once you have this entitlement, your device and app will show up in the Develop menu and you can attach to it.
And it's easy just that easy to connect Web Inspector to your JSContext and WebViews.
Okay. So we're up and running with Web Inspector and our apps.
Time to move onto some new features in Web Inspector.
And the reason is simple.
The new profiler uses a sampling technique that doesn't affect your performance anywhere near as much as before.
So we had a profiler in an era before there was really a compiler.
It was an interpreter.
But now we have this very powerful four-tier JIT compiler, and the right profiler for that is really a sampling profiler.
And the sampling profiler tells you where time is being spent in your code.
It helps you answer questions like what code is costing us the most time?
It samples the running program every millisecond, and it pauses execution briefly to take a quick snapshot of all the code that's running.
And it can also take samples while running your code with all four tiers of a JIT enabled.
So that means it's sampling at near the true speed of your code.
And since handling breakpoints can cause code de-optimization, we temporarily ignore them.
So while you're profiling, you get the truest performance for your web app.
There isn't nearly the same performance cost to using the sampling profiler.
And that literally means while profiling your code, it can run at up to 30 times faster.
It makes the whole process of profiling your code much quicker and easier, and you get much more accurate data as a result.
This was such an exciting development that our team, our Web Inspector team, was able to take advantage of this to find places where we could improve the speed in Web Inspector itself.
So we have a sampling profiler.
Let's see how Web Inspector uses it to help us find problems.
So there's a lot going on here, but it's actually pretty easy to break down.
And it's even more helpful with code you're already familiar with.
In particular, this is for some code that uses the D3 library, so it's even helpful for debugging code and profiling code that's in a library that you're using.
And each entry here is an event where code is executed.
And that includes event listener callbacks, like these animation frame entries for request animation frame handlers.
And we also have some timing information here, showing you the time cost of code that's being run.
So if anything is more than 10 or 15 milliseconds, you're getting really close to dropping below that smooth 60-frames-per-second performance.
Now the Events view is helpful but there's another view that we've added for you and that's the Call Trees view.
If you've used other profiling tools, this ought to be pretty familiar to you.
Just click this Menu and switch to the Call Trees view.
And now it shows you the accumulative time for functions in the call stack.
And this is what we call the Top Down view, and you can use it to dig down through the Call Tree to uncover hot functions spending lots of time.
But my favorite view is the Bottom Up view.
It takes me right to the hottest functions, the functions that are sampled most often.
And this is the list of the called functions, and it's sorted by the ones costing the most time.
So it inverts the Call Tree so you can quickly compare the function costs directly.
And you can see exactly where most of your time is being spent.
You can expand the entries and go back up the path that leads to the functions chewing up all the time.
And this tells you when and where your most expensive code is getting called.
And to see this in action, I'd like to invite my colleague, Brian Burg, to the stage for a demo.
[ Applause ]
So the sampling profiler is great because it can take really complex content and still profile it and get you really accurate information.
And you can make it even faster.
So to show this off, I've got this iPad app I made.
It's called Satellite Tracker.
Let me get the display here.
So Satellite Tracker will show you, right now or any time, where the satellites are overhead.
So you can choose different places on Earth.
You can choose different satellites.
So that's great.
If you're worried about satellites overhead and want to put on a tinfoil hat when overhead, this is a great app.
But there's a small problem is that if we have a lot of satellites overhead, or pieces of a satellite that blew up, in this case, the frame rate's kind of choppy.
This is definitely not 60 frames per second.
It's jittering all over.
So we can use the sampling profiler to figure out what's going on and why it's so slow.
So what we've got to do is go over to Safari and we go to that Develop menu.
Find the iPad here and attach to it.
And the first thing I want to do is go over to the Frames view here and just see where we are right now, in terms of frame rate.
So let's just start recording.
I'll switch back to my iPad.
And start doing stuff.
It'll sort of spin around here.
Maybe a change the satellites.
Change the time.
Okay. Let's go back and see.
Okay. Let's zoom way out.
Yeah. Our performance is all over the place here.
So sometimes we're getting 60 frames per second here on the left.
In the middle, it's just sort of going all over the place as we're changing the views.
And here we're sort of going too slow.
So as Jon showed before, the Events view here is going to show everything that went into the run loop.
And in this case, it's an animation or simulation, so we're just rendering frames over and over.
So it's not really helpful, if we want to figure out what's taking the most time.
So let's switch over to the Call Trees view, and here we see that Top Down Call Tree.
And this shows aggregated over all those rendering frames where we spent the most time.
And here we can expand this to see that D3 has a Timer function and that calls some of our code, which draws a scene.
And you know we draw some things in the scene like satellites and the time and these things.
So this is great, if we want to understand what the code is doing.
But if we want to figure out which functions specifically are really hot, it's better to go over to the Bottom Up view.
So here we've listed all the Functions regardless of who called them.
And we can see that fillText and our tangent [phonetic] are our two hottest functions, so why are we calling our tangent?
We can expand out this row here and see who's calling our tangent.
So right here, we're plotting some satellites.
It looks like we're computing the transform so we can draw this globe.
Okay. These things seem pretty normal.
Maybe I can pull out my math book and make it a little faster.
Let's go up to fillText.
So and to refresh you, if we go back to the app here, we're drawing text on the current time up there and also for every data point.
So that makes sense.
But if we look really closely here, we're actually drawing the time twice, and that seems kind of strange.
So if we expand this out, we can see who's calling this, and it seems that we're seem to be drawing two different foreground scenes at once.
This is probably not what we wanted.
So let's figure out what's going on.
If it was the case that we were drawing two foreground scenes, then we're doing twice as much work as we need to.
So over here, just to refresh your memory, we have this sort of flat map.
And then we have the globe, which rotates.
So let's go back into the code, and figure out what controls switch in-between these two maps.
Maybe we messed up somewhere.
So we switch between the two globes when we change the location.
So here's updateLocation.
Okay. So when we have one map up, we don't see the other one.
So that makes sense because here we're adding the hidden class, and that's just going to make it not display.
And here for the globeMap, we set running equal to true when the whole thing is running.
And also in the place we're showing it's not a global projection, so that makes sense.
When we're showing the flat map, we don't show the globe when it's not running.
Up here for the flat map, it seems like it's always running if the UI is running at all.
So that's kind of strange.
Well let's go back to the map and try something.
So we'll go up to our [inaudible] data set here.
And if we go to Earth, it seems to have a better frame rate than if we just did the globe map.
And well, that makes sense.
I think we're drawing two maps when the globe map is active but only one map when we're doing the flat maps.
So if we go in here and change this condition We want it to be the opposite.
Okay. So let's stop, and see if this is the fix.
So go back to our iPad here.
Okay. This looks pretty smooth.
Let's go here.
Oh that's great.
Yeah, it looks really nice.
So let's go and check that rendering timeline again and see if it's 60-frames per second.
So I'll switch back to Frames again and start recording and yeah.
That's pretty nice.
I'm spinning the globe, and it slows down a little bit.
But the steady state seems to be okay, we're definitely under 60-frames per second.
And over here we have the bar, and if we're under it, then we're in luck.
Okay. So now Satellite Tracker's a lot faster, so we know exactly when to put on our hat.
This is great.
So this is a small example of how we can use a sampling profiler to dig into really busy content and make it even faster.
Jon's going to tell us about memory and allocations.
[ Applause ]
Thank you Brian.
So you can see that profiling is fast.
It allows you to see the true speed of your code so you can get really accurate data.
And use the new Call Trees view to see time cost, as they pile up across the time slice you select.
Remember that Bottom Up is your new best friend.
It really helps you find the best places to start optimizing.
I'm really excited that we have better tools for optimizing CPU time.
And we can give our users a fast experience and save a bit more of their battery life.
And this is great and now we're going to move on to the other side of the performance coin, figuring out where the memory is going.
So you want to be efficient with memory in your web content because it's a limited resource.
Being memory efficient helps your web content be able to scale really well to handle large data sets.
Plus, using lots of memory degrades performance and we don't want to do that.
and it can also bring down your web content and we really don't want to do that.
Now the good news is if you're going WKWebView, it runs in a separate process, so it won't bring down your entire app.
But still, it's not a great user experience.
So to help you with all of this, we've added two new timelines to Web Inspector and Safari 10.
When you fire up the new Web Inspector, the new timelines will be off by default, so you need to turn them on.
You just click the Edit button and just above the Timelines, you can now configure the timelines you want to see.
So you can just work with the ones you want to work with.
Just like in the Instruments app.
So just toggle the new timelines On, and you're ready to record a new timelines.
But you probably don't want to keep them all running at the same time.
Okay. So we're going to leave the Memory Timeline turned On.
And when you record a timeline, you get something like this.
You get this new Memory Timeline graph and it shows you how memory has been allocated across different categories over time.
And there are a series of charts and graphs to help you understand how memory is being used and how it's being divided up.
And the Max Comparison chart helps you investigate memory spikes.
So we have a high watermark here and that helps you see memory problems in the past.
And you can even isolate spikes, by selecting a specific slice or a specific range of time around a spike in the timeline.
And then, you can use the category breakdown below to see what's contributing most of the spike.
And each of these graphs here are independently scaled.
So you can easily see changes over time.
And that includes Objects like string Objects and functions and all the engine data that supports them like structure data and compiled code.
And images shows you the memory allocated for images that have been decoded for display, so that's the larger image data, usually used for images that are visible in the viewport.
And layers is showing you graphics layer memory, memory used for WebKit's tile grid, compositing layers and other engine layers.
Pages is everything else, all the other things the engine's keeping track of, like the DOM and page styles, fonts rendering data, memory caches and system allocations.
So this breakdown gives you a great way to ensure that memory use lines up with your expectations.
And you'll likely see more changes over time in the Timeline graph.
But for an image-heavy page like a gallery, for example, then the layers and images categories would likely be the largest, with more changes over time.
So that's the Memory Timeline, new in Safari 10.
And you can dig in to see everything that's allocated.
But it's even more powerful when you have two Snapshots, and this allows you to go back later and compare the two.
And comparing Snapshots is one of the most powerful tools for answering the question, am I doing unnecessary allocations?
So to really make use of this, you need multiple Snapshots.
So that's why, by default, we take one every 10 seconds and also at the beginning and end of a recording.
So the Snapshots are plotted on the Timeline, so you can correlate them to things happening on other timelines.
I just have the others turned off here for now.
And the Snapshots are listed below with a few details, like timing and size of the heap.
Now, to dig into a particular problem, you'll often need a Snapshot, both before and after where you think a memory issue is happening.
And there are three techniques.
You can rely on the automatic ones, every 10 seconds.
Or you can take one yourself by pushing the Take Snapshot button.
Or you can do it from your code.
And really the easiest way to zoom into an issue is to modify your code a little.
You call the takeHeapSnapshot API and pass it a custom label argument of really anything that can help you identify it later.
And again, you want a pair of Snapshots, both before and after the code you think is causing the problem.
Now you could also use this by taking a Snapshot between doing some work in a loop.
So just some quick things to keep in mind about the takeHeapSnapshot API.
Remember that Snapshots do add some extra process during garbage collection and that can impact performance, which you'll definitely notice if your code is firing off a lot of Snapshots rapidly.
You'll also want to capture the differences before and after code that's doing some work or at some point between work in a loop.
And don't leave this in.
I mean, if you leave this in, for most customers it'll be okay.
But if anybody's running Web Inspector, they're going to be taking all these Snapshots, and you probably don't want that.
So just remember to be sure and take it out before you ship.
So what are these Snapshots really show you?
Let's take a look.
You just click on the Snapshot icon on the Timeline or on the Arrow button of any of the Snapshot list entries.
And you'll get this list of Objects that were allocated in the heap.
And we have two views for Snapshots.
This is the Instances view, and it shows you a list of Objects in the heap, grouped by their class.
And the other is the Object Graph view.
And this is really an overview of everything and everything that's owned by everything.
So if you're readily familiar with the code, this can be a useful way to confirm things or where they should be.
But actually the far more useful view is back in the other one, in the Instances view.
And it's powerful because you can easily find Objects no matter how deep-down the property path they are.
And the Count here can help you realize potential issues when they don't meet your expectations.
Like was I really expecting over 4000 string Objects?
So you can expand the Classes and see all of the allocated Objects of that class.
Then to figure out what something is there's all these different clues.
The Class is a clue.
Another is the actual properties of the Object.
It's a really quick way to know what it is.
But the easiest way to know what an Object is is to hover over this Object Identifier here and you get this.
Look at this.
It literally shows you the shortest path to the Object.
This is telling you exactly what is keeping the Object alive.
This is the kind of thing that cuts right through the confusion.
But the most important feature and really the entire point of this is to be able to compare two Snapshots.
Now watch this.
Once you've collected some Snapshots, you just click the Compare Snapshots button here, and you select a Baseline Snapshot and a second to compare against.
And boom. You get a new Comparison Snapshot to dig into.
Now this is a really big deal because now we're only seeing the Objects that are new between two points, between our two Snapshots.
Expanding the Object Class group we can see all of these Object Allocations.
And their previews are showing their names and what looks like some telemetry data.
And that's a clue that these are Satellite Objects.
And the pop over here shows that they're in the Satellites Array property.
Since this is a Snapshot comparison, these Satellite Objects are all newly allocated.
And that's a big clue as to what the code's doing.
So to show you all of these new memory features in action, I'd like to invite Brian back up to the stage for another demo.
[ Applause ]
So I gave Satellite Tracker to my buddy, Ed, and he stayed up all night playing with it.
And he had a lot of fun, and he never got tracked by the satellites.
But there's a problem.
Eventually it just kept getting slower and slower the longer it was up.
And to me that sounded like you know classic memory.
The longer the thing is open, the slower it goes.
So I want to look into Satellite Tracker with these new memory tools to figure out if we're leaking some memory somehow.
So the first thing I'm going to do.
Okay I've got iPad here.
I'm going to go back to the Web Inspector And inspect the app.
And the first thing I like to do, when I don't really know what the bug is, here is I want to use the Memory Timeline.
And that's going to show me sort of like an overview of what's going on on the page.
So let's start recording.
I'll switch back, and I'm just going to switch back and forth between two satellites.
Maybe I'll add some effects here.
Okay, I'm switching back and forth.
Okay. Let's see what's in the Timeline.
So in the Timeline overview you can see a stacked line graph showing all the different parts and the relative size, but if you click here, you'll get that more detailed view.
And so there's no images on this page.
This is all canvas.
The layers is pretty flat.
The page sort of goes up and down, stuff's getting garbage collected.
And if we had this running all night, yeah, it would probably go up a lot more.
So the next step here is to start taking heap Snapshots or allocation Snapshots so we can figure out what's being allocated over time.
So to do that, we're going to start a new recording.
And one cool quick trick here is you can do Shift Click or Shift Space, and they'll start a new recording and not append to the old one.
Oh wait. I forgot to change our Timelines here.
So let's put away Memory.
And let's put in Allocations instead.
Okay. So let's start recording.
Go back to the iPad.
Here I added this little takeHeapSnapshot button, so I already added some calls to the console about takeHeapSnapshot when we switch between the two satellite groups and some other actions.
So for this recording, I'm going to rotate and then I'm going to switch between two satellites over and over.
We should look at Spy Satellites.
That seems kind of relevant.
So this you know we're making lots of Objects, so this is going to slow down your app a lot.
So it's important to not take lots of Snapshots.
You want to take them only on important times.
So here, you can see these Ss in the box and those are the Snapshots we took.
So if we zoom in over here, we can see that there's pretty steady memory growth over time as we start switching between these satellite sets.
So if we want to investigate this, like Jon said, we need to start comparing two of these to see what's being retained at the end.
So let's go between Snapshot 9 and 11.
And right away, we can see a bunch of stuff that was allocated between Snapshot 9 and 11 and is still alive right now.
So that's a pretty good sign that it's being retained and you know we probably didn't mean to do that.
So we can start looking at what these things are.
There're some Arrays.
It looks like we have Arrays full of coordinates.
And you know we use coordinates in lots of different places in this app, but if we hover here, we can see the path to these things.
So these seem to be saved in trajectoryHistory, which is what we use to make those trails behind the satellites.
So okay. That's fine, but I don't think we should still have this trajectoryHistory for satellites who are no longer showing.
That seems kind of like a bug.
We allocate some Objects too.
And this is sort of strange because you know between Snapshot 9 and 11, we've already seen these satellites before.
So I wouldn't expect that we're making new Objects for each satellite.
We should just use them if we already fetched the resources for them.
So and then here's a bunch of coordinates and telemetry stuff, so it seems like we might be like re-parsing them or something.
But I'm not sure, so one thing I'd like to do in this view is you know we have lots of Objects.
You can't read all of them, but what I like to do is find something that's fairly unique.
And in this Snapshot here, we have lots of Strings, lots of Arrays, lots of Objects.
But there's only one Promise that's retained between these two Snapshots.
So I think if I want to debug this, I should start looking in our code to see where we're using Promises because it seems that that's being leaked with some other stuff.
So we search for Promise.
Okay. There's D3 library, and here's our code that uses it.
Okay. Let's go to this one.
Okay. So in the [inaudible] I did, we switched between the satellites a lot and then the code that's called loadDataset.
So someone left comments.
So here it looks like someone requests we change the satellites.
So here we asynchronously load that data for the satellites from the TRL.
When it comes back, we're going to parse it, and then we're going to parse it some more with the satellite plotting library.
And then we're going to save it to our list of satellites.
That's all well and fine, but back in Inspector, it seems that we're leaking this Promise every time.
So if you look more carefully, what if we switched to a data set that we already loaded before?
It seems like we're not even checking for that case.
So if you look more carefully up here, every time we switch between the two satellites, we're doing a new network request.
So if we go to this Timeline, it seems like we're requesting the same debris field data over and over.
And that makes sense because if we go back to the code, we request that.
Then we make a bunch of Objects when we parse it.
And then we push it onto an array of satellites, and that thing never really gets cleared as far as I can tell.
So it seems like we're just doing a lot of unnecessary work and then leaking it.
So I think what we can do here is to check if we already have this parsed satellites Object.
Because this is a Promise, if this thing already exists, we can still call .then on it.
Since it's already solved, next time we evaluate the Promise reactions, it's going to go through and set the satellites onto the map.
So let's add a [inaudible] here.
And if this doesn't exist, then we'll make it Okay. Let's stop and rerun and see if this fixes this.
So we need to go back and reattach And okay, here's our app, and we're going to start recording.
And when we go back to the app, I'm going to turn on the Snapshots.
Turn on some effects.
And we'll go down south.
Okay. So Spy Satellites.
Science Experiments by Satellites.
So if we go back here, we're seeing a lot less memory growth.
Maybe 1 megabyte, instead of like 4 or 5, so there might be some more leaks in here.
But at the end of the Snapshot, we have about as much memory as when we started rendering this thing in the beginning.
So I think we've fixed that particular leak.
So this shows how we can use the Allocations and Memory Timelines to figure out where we're leaking memory in apps like this.
And it's great because like this app has a lot of stuff going on.
But still, with the dipping [phonetic] functionality, we can really drill down to what's changing in some Timeline we care about.
Okay. So that's the end of Satellite Tracker.
Back to you, Jon.
[ Applause ]
Thank you Brian.
You can see how it really amazingly quick and simple these new Timelines make it to zero in on memory issues.
So remember you want to use the Memory Timeline to figure out how memory is being used and what's driving memory spikes, so you have an idea of where to go look.
Also, don't forget to get rid of takeHeapSnapshot from your code before shipping.
So that's a look at the new Timeline instruments available in Web Inspector with Safari 10.
I think you're really going to love using them.
And as I wrap this up, I want to leave you with some next steps to take.
I want to encourage you to reconsider WKWebView, if you've not made the switch yet.
And turn on the Develop Menu in Safari Preferences, connect Web Inspector to your app and start taking advantage of these new features.
Save a ton of time using them with the Bottom Up view and Call Trees for the best places to start optimizing.
The Memory Timeline to quickly see how memory spikes are happening.
And Heap Snapshots to easily explore and compare Object allocations.
And stay updated on features.
There's more this year in Web Inspector and WebKit that you can take advantage of in your app's web content to deliver great in-app experiences.
Along with the features Brian and I showed you today, our team added some other features to Web Inspector over this past year.
Quick Open will jump you right into the resources loaded with your page.
And Tail Call Stacks will now show you Tail Call optimized functions in the debugger.
And earlier this spring, we shipped Safari 9.1 on OS 10, and with an updated Web Inspector with it.
And that shows the Pseudo Elements in the DOM Tree of the Elements tab.
And there's also the new visual style sidebar.
And if you aren't already aware, Web Inspector is a developer tool that's created as part of the WebKit Open Source Project.
WebKit is the web browser engine that's used to power your apps and drive WebViews and JSContext.
And of course, it's also the engine behind Safari.
In this past year, our teams added great new features to WebKit.
We hit 100% support for ES6.
We improved support for the recommended IndexedDB Standard.
We also added Shadow DOM support and WebDriver, CSS Variables and the Picture Element.
So there's a lot going on and as an open source project, you can follow development as it occurs.
Most of you here will want to take advantage of this.
And if there are some of you here that want to enhance some of these, you have that opportunity because of the way WebKit is an open source project.
So you can find out more about the WebKit Project on WebKit.org.
And the WebKit team, the engineers behind WebKit, blog about development work.
In fact, we have two blog posts up now that cover the Memory Timelines and the Sampling Profiler that we talked about today.
So if you want to get more information, you can dig in there.
We also have a feature status page that gives you at-a-glance updates for our web standards progress.
And there's also links to downloads for WebKit nightly builds and our latest browser, the Safari Technology Preview.
It's updated every couple of weeks, with an updated WebKit engine, so you can try out new and experimental features in WebKit every two weeks as it's improved.
Our teams pore a lot of work into Safari WebKit and Web Inspector.
And the Web Inspector team was able to use these new performance features we showed you today to find issues and deliver faster performance in Web Inspector itself.
I can't wait to see what you'll do with them.
So for more information, you can watch this session and download the slides at developer.apple.com.
There are lots of other relevant sessions earlier this week as well as in past years.
You can find those on developer.apple.com as well.
So on behalf of Brian, myself, Safari and the WebKit teams, thank you for being here.
enjoy the rest of WWDC.
[ Applause ]