Optimizing App Performance with SC.Benchmark

written by Peter Bergstrom

SproutCore is a powerful JavaScript framework that allows developers to take full advantage of the code design paradigms used for native applications developed in languages like Cocoa. However, the tools that are available for web development are different and, in most ways, more basic than what is available when writing native applications.

One of the major challenges developing SproutCore applications is measuring the performance of your application. The major web browsers all have built-in profilers that allow developers to examine the overall performance of their application. These profilers allow you to easily track the execution of operations from start to finish, but they do not provide an easy way to optimize specific portions of code.

One of the less known features of SproutCore is SC.Benchmark, a built-in benchmarking tool that developers can, and should, use to optimize their application. There are several ways to report the data captured using SC.Benchmark, including aggregate reports and graphs.

It is very simple to use and can be used anywhere in a SproutCore application. Each benchmark needs a key value. This key value does not need to be unique if you want to get averages for benchmarks over many runs of a code segment. To start the benchmark, you add SC.Benchmark.start("my benchmark key"); to your function. To stop it, you add SC.Benchmark.end("my benchmark key");

Benchmarking Functions and Loops

In the example below, you can see it used inside of a function to capture the execution time for creating a number of SproutCore objects.

createObjects: function(klass, count) {
	SC.Benchmark.start("createObjects");
 
	var objects = [];
	for(var i=0; i<count; i++) {
		objects.push(klass.create());
	}
 
	SC.Benchmark.end("createObjects");
}


Let’s say that you also want to capture how long it takes, on average, to create each object. In this case, you can add another benchmark:

createObjects: function(klass, count) {
	SC.Benchmark.start("createObjects");
 
	var objects = [];
	for(var i=0; i<count; i++) {
		SC.Benchmark.start("createObjects: creating one object");
		objects.push(klass.create());
		SC.Benchmark.end("createObjects: creating one object");
	}
 
	SC.Benchmark.end("createObjects");
}

Now there are two different benchmarks captured. First, there is the total time for running the whole function, then there is also the average time for creating objects in the loop.

As with most benchmarks, the more times you run the benchmark and average the runs, the more accurate it will be. To get the output for a benchmark where you are looping over many iterations, run SC.Benchmark.report(), which will print a basic report to the console.

The report for the function above, when run 100,000 times, looks like the one below. It shows that the function as a whole took 441 milliseconds and it took on average 0.003 milliseconds per iteration of the loop when creating the 100,000 objects.

1
2
BENCH 441 msec: createObjects (1x); latest: 441
BENCH 0.003 msec: createObjects: creating one object (100000x); latest: 0"

Benchmarking Loading and Application Flow Using timelineChart()

In my opinion, the best place to use SC.Benchmark is when you’re optimizing the load time of your application, and not when you’re trying to optimize a loop over many iterations. In the case of optimizing the load time, you can add benchmarks to the major functions inside of the application to capture the order and duration of the time spent in various parts of the app. Then, using the SC.Benchmark.timelineChart(); command, you can view an easy-to-understand cascading graph that depicts where in the application time was spent.

As an example, take a sample application, MyApp, that has a state chart that is initialized in the apps main() function and goes to the first state that shows a loading view while fetching some data from the server. Once the data is loaded, it is shown to the user in another state. Below is the code for the application.

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
main.js:
 
// ==========================================================================
// Project:   MyApp
// Copyright: ©2011 My Company, Inc.
// ==========================================================================
/*globals MyApp */
 
/** @namespace
 
  My cool new app.  Describe your application.
 
  @extends SC.Object
*/
MyApp = SC.Application.create(
  /** @scope MyApp.prototype */ {
 
  NAMESPACE: 'MyApp',
 
  VERSION: '0.1.0'
});
 
statechart.js:
 
// ==========================================================================
// Project:   MyApp - statechart
// Copyright: ©2011 My Company, Inc.
// ==========================================================================
/*globals MyApp SC */
 
sc_require('resources/loading_page');
 
MyApp.statechart = SC.Object.create(SC.StatechartManager,
/** @scope MyApp.statechart */ {
 
  autoInitStatechart: NO,
  initialState: 'showLoadingUI',
 
  showLoadingUI: SC.State.extend({
    enterState: function() {
      SC.Benchmark.start('showLoadingUI: enterState');
 
      SC.Benchmark.start('Append loadingPage');
      MyApp.loadingPage.get('mainPane').append();
      SC.Benchmark.end('Append loadingPage');
 
      // Dummy request.
      SC.Benchmark.start('Retrieve Dummy Data');
      var responseHandlerFunc = function(response) { 
        MyApp.statechart.sendEvent('didRetrieveResponse', response);
      };
      SC.Request.getUrl('http://localhost/dummy')
                .notify(this, responseHandlerFunc)
                .send();    
 
      SC.Benchmark.end('showLoadingUI: enterState');
    },
 
    exitState: function() {
      SC.Benchmark.start('showLoadingUI: exitState');
 
      SC.Benchmark.start('Remove loadingPage');
      MyApp.loadingPage.get('mainPane').remove();
      SC.Benchmark.end('Remove loadingPage');
 
      SC.Benchmark.end('showLoadingUI: exitState');
    },
 
    didRetrieveResponse: function(context) {
      SC.Benchmark.end('Retrieve Dummy Data');
      this.gotoState('showResults', context);
    }
  }),
 
  showResults: SC.State.extend({
    enterState: function(context) {
      SC.Benchmark.start('showResults: enterState');
 
      // and so forth...
 
      SC.Benchmark.end('showResults: enterState');
    }
  })
 
});
 
resources/loading_page.js:
 
// ==========================================================================
// Project:   MyApp
// Copyright: ©2011 My Company, Inc.
// ==========================================================================
/*globals MyApp */
 
MyApp.loadingPage = SC.Page.design({
 
  mainPane: SC.Pane.design({
 
    childViews: ['loadingView'],
 
    loadingView: SC.TemplateView.design({
      templateName: 'loading'
    })
  })
});

In this case, you can see that even the transit time to and from the server is captured in a benchmark. This is important because you’ll know how long the server takes and also how long the application is waiting. In some cases, if you know that the server will take at least a certain amount of time, you can optimize your application to use that time to load other parts of the application as needed.

While this will create many data points, they are quite useful when your functions and sequences of calls become more complex as your application grows. When you run SC.Benchmark.timelineChart(), in the MyApp application, you see the following:

There are several interesting things to note here. 5 milliseconds are spent in the head and 348 milliseconds are spent loading the body. This is longer than it would be when deployed after a build because it’s run using sc-server in production mode.

After 367 milliseconds, the application’s main function runs. Contained within it, the appending of the loadingPage occurs in the showLoadingUI state’s enterState function, taking 40 milliseconds. After the loading page is appended, the dummy data is retrieved from the server, taking 41 milliseconds. Once it’s retrieved, the loadingPage is removed in the exitState function of the showLoadingUI state.

In real applications that you may actually write, the load time and processing will take longer and you will see in more detail, where the time is spent.

Capturing data points in the index.rhtml file

While it’s nice to capture data points during the execution of the SproutCore code, it’s also important to know what is happening during the loading of the JavaScript in the index.rthml file. By default, there are the head and body data points. However, if you want to add additional capture points, you can do the following inside your index.rhtml file:

1
2
3
4
5
6
7
8
9
10
 
 <script>
  SC.benchmarkPreloadEvents['myOtherIndexRHTMLBenchmarkStart'] = new Date().getTime();
  </script>
 
  <!-- Your other HTML, JavaScript, CSS that you want to benchmark can go in here. -->
 
  <script>
  SC.benchmarkPreloadEvents['myOtherIndexRHTMLBenchmarkStart'] = new Date().getTime();
  </script>

Conclusion

Using SC.Benchmark is a very useful way for developers to capture data about the performance of their SproutCore applications. When used in conjunction with built-in browser profilers, it is possible to optimize the loading as well as run time performance of a SproutCore application to meet the required needs. Performance is especially important when dealing with applications that are targeted for mobile devices, and using SC.Benchmark can help you squeeze out that last ounce of performance that you may need to deliver a kick ass user experience.