Stopwatch is a class that helps you with Profiling Your App to measure the amount of time that your app spends in foreground and background mode. When you add it to your app, it displays a transparent banner in the corner that shows the amount of time in each mode, as well as the percent of the total time spent in foreground mode.

The stopwatch class can be added to any app that uses the Senza Client Library. For this demo, we'll add it to a copy of the Unified Player app in the Reference Integration.

How it works

The Stopwatch class listens to events from the Lifecycle object to detect when the app moves to foreground and background modes. It then stores the number of seconds the app spends in each mode:

  • When the app is in the foreground, it runs a timer that increments the counter once per second.
  • When the app is in the background, it may be suspended after a period of time so we can't rely on a timer. Therefore the app records the current time when moving to background, and calculates the elapsed time when returning to the foreground.

The app uses session storage for Preserving State when the app is suspended. The totals will maintained for the duration of the current session, such as when a device is running the app or the Simulator is connected.

Setup

You can follow along with the implementation in the stopwatch.js file. The file imports the Lifecycle object from the Client Library, and exports a class called Stopwatch. Create a constructor method that we'll add some code to as we go.

import { lifecycle } from "senza-sdk";

export class Stopwatch {  
  constructor() {  

  }  
}

Data Model

The class will use three instance variables to store its data:

  1. foreground — number of seconds spent in foreground mode
  2. background — number of seconds spent in background mode
  3. backgroundTime — the time when the app most recently entered background mode

It defines two methods to save the state to session storage, and restore the state from session storage:

restore() {  
  this.foreground = parseInt(sessionStorage.getItem("stopwatch/foreground")) || 0;  
  this.background = parseInt(sessionStorage.getItem("stopwatch/background")) || 0;  
  this.backgroundTime = parseInt(sessionStorage.getItem("stopwatch/backgroundTime")) || 0;  
}

save() {  
  sessionStorage.setItem("stopwatch/foreground", `${this.foreground}`);  
  sessionStorage.setItem("stopwatch/background", `${this.background}`);  
  sessionStorage.setItem("stopwatch/backgroundTime", `${this.backgroundTime}`);  
}

Add a call to restore() in the constructor to initialize the state in case the app was restarted.

Counter

We'll implement a counter that increments the foreground total once per second while in foreground mode. This code will also update the banner with the latest totals.

start() {  
  this.updateBanner();  
  clearInterval(this.interval);  
  this.interval = setInterval(() => {  
    this.foreground++;  
    this.updateBanner();  
  }, 1000);  
}

stop() {  
  clearInterval(this.interval);  
}

In the constructor, create an instance variable called interval, and add a call to start()to start the timer when the app launches.

Lifecycle

In the constructor, add a handler that listens for the lifecycle'sonstatechangeevent, and depending upon the new state calls themovedToBackground()ormovedToForeground() methods.

When moving to background, we save the current time, stop the timer, and save the state.

When moving back to the foreground, we calculate the amount of time that we've been in background mode, which is just the difference between the current time and the time that we saved, and add that to the total time we've spent in the background. Then we start the timer again.

constructor() {  
  lifecycle.addEventListener("onstatechange", (event) => {  
    if (event.state === "background") {  
      this.movedToBackground();  
    } else if (event.state === "foreground") {  
      this.movedToForeground();  
    }  
  });  
}

movedToBackground() {  
  this.backgroundTime = Date.now();  
  this.stop();  
  this.save();  
}

movedToForeground() {  
  if (this.backgroundTime) {  
    this.background += Math.ceil((Date.now() - this.backgroundTime) / 1000);  
  }  
  this.start();  
}

Banner

In order to display the totals, we'll programmatically create a divelement and add it to the body. We'll apply some style attributes to pin it to the top left corner, give it a transparent background, and make it float above other elements on the page.

createBanner() {
  this.banner = document.createElement('div');
  this.banner.style.position = 'fixed';
  this.banner.style.top = '0';
  this.banner.style.left = '0';
  this.banner.style.backgroundColor = 'rgba(0, 0, 0, 0.4)';
  this.banner.style.color = 'white';
  this.banner.style.fontFamily = 'monospace';
  this.banner.style.fontWeight = '500';
  this.banner.style.fontSize = '24px';
  this.banner.style.padding = '22px';
  this.banner.style.display = 'flex';
  this.banner.style.zIndex = '1000';
  document.body.appendChild(this.banner);
}

Add a call to createBanner() to the constructor.

While we're in foreground, we'll update the banner every second so the clock ticks. We'll add a little formatTime() function to format the time in seconds in the 0:00:00 format. We'll also calculate the ratio of time spent in foreground mode as a portion of the total time, and format it as a percentage.

updateBanner() {
  let ratio = this.foreground ? Math.floor(this.foreground /
    (this.foreground + this.background) * 10000) / 100 : 100;
  this.banner.innerHTML =  `Foreground: ${this.formatTime(this.foreground)}<br>`;
  this.banner.innerHTML += `Background: ${this.formatTime(this.background)}<br>`;
  this.banner.innerHTML += `${'&nbsp;'.repeat(4)} Ratio: ${ratio.toFixed(2)}%`;
}

formatTime(seconds) {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = seconds % 60;
  return String(hours) + ':' + 
    String(minutes).padStart(2, '0') + ':' + 
    String(secs).padStart(2, '0');
}

Adding the class

You can add the Stopwatch class to your existing app by importing the class, creating an instance of the class in the window's load handler, and assigning it to a global variable. You don't need to import any HTML or CSS resources, as the banner element is created programmatically. Here's an example of how we've added the class to a copy of the Unified Player reference app.

import { init, uiReady } from "senza-sdk";  
import { UnifiedPlayer } from "./unifiedPlayer.js";  
import { Stopwatch } from "./stopwatch.js";

const TEST_VIDEO = "<https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd">;

let unifiedPlayer;  
let stopwatch;

window.addEventListener("load", async () => {  
  try {  
    await init();  
    unifiedPlayer = new UnifiedPlayer(video);  
    stopwatch = new Stopwatch();  
    await unifiedPlayer.load(TEST_VIDEO);  
    unifiedPlayer.play();  
    uiReady();  
  } catch (error) {  
    console.error(error);  
  }  
});

Trying it

If you run the updated app in the simulator or on a device, you'll see the banner in the top left corner. The foreground timer will start ticking immediately, and you'll see that so far we've spent 100% of the time in foreground mode.

If you switch to background mode (in this app by hitting OK), wait a while, and then switch back to the foreground, you'll see the background time has been incremented by the amount of time you were there. The percentage will now show a meaningful number.

Digital Signage

As you have seen, the percentage of time spent in foreground mode depends entirely upon on how the user interacts with the app using the remote control. Digital signage apps are typically less interactive, and display content according to a script.

For example, the Chocolate app alternates between two modes:

  1. Showing some HTML content for ten seconds
  2. Playing one of several videos, which range in length from 10-25 seconds

If you run the app from https://senzadev.net/chocolate in the Simulator, you'll see that as it runs for a while, the ratio stabilizes at around 40%. That gives you a good idea of how much foreground time the app will consume if you run it all day during business hours.