Looking for performance tuning ideas

The following code is a version of the usual function required in an analog watchface to rotate the various shapes (which all are polygons with four corners in my case). It works fine but is now one of two functions that take the most time. So I was wondering if it can be further tuned. I've tried with a lookup table for sin/cos - which doesn't make a difference - and with a 1-dimensional array for result - which is even slower - and now I'm out of ideas.

Does anyone see any way to further tune this code to make it faster?

    //! Rotate the four corner coordinates of a polygon used to draw a watch hand or a tick mark.
    //! 0 degrees is at the 12 o'clock position, and increases in the clockwise direction.
    //! @param shape Index of the shape
    //! @param angle Rotation angle in radians
    //! @return The rotated coordinates of the polygon (watch hand or tick mark)
    private function rotateCoords(shape as Shape, angle as Float) as Array< Array<Number> > {
        var sin = Math.sin(angle);
        var cos = Math.cos(angle);
        var shapeIdx = shape * 8;
        var result = new Array< Array<Number> >[4];
        for (var i = 0; i < 4; i++) {
            var idx = shapeIdx + i * 2;
            var x = (_coords[idx] * cos - _coords[idx + 1] * sin + 0.5).toNumber();
            var y = (_coords[idx] * sin + _coords[idx + 1] * cos + 0.5).toNumber();
            result[i] = [_screenCenter[0] + x, _screenCenter[1] + y];
        }
        return result;
    }

_coords is a long Array<Number> with the coordinates of all my shapes. The complete code is here.

  • Try to expand the for loop and avoid repeating same operations

     
        //! Rotate the four corner coordinates of a polygon used to draw a watch hand or a tick mark.
        //! 0 degrees is at the 12 o'clock position, and increases in the clockwise direction.
        //! @param shape Index of the shape
        //! @param angle Rotation angle in radians
        //! @return The rotated coordinates of the polygon (watch hand or tick mark)
        private function rotateCoords(shape as Shape, angle as Float) as Array< Array<Number> > {
            var sin = Math.sin(angle);
            var cos = Math.cos(angle);
    
    		var offSetX = _screenCenter[0] + 0.5;
    		var offSetY = _screenCenter[1] + 0.5;
    
    		var idx = shape * 8;
    		var idy = idx + 1;
    		var x0 = (_coords[idx] * cos - _coords[idy] * sin + offSetX).toNumber();
    		var y0 = (_coords[idx] * sin + _coords[idy] * cos + offSetY).toNumber();
    
    		idx = idy++;
    		idy++;
    		var x1 = (_coords[idx] * cos - _coords[idy] * sin + offSetX).toNumber();
    		var y1 = (_coords[idx] * sin + _coords[idy] * cos + offSetY).toNumber();
    
    		idx = idy++;
    		idy++;
    		var x2 = (_coords[idx] * cos - _coords[idy] * sin + offSetX).toNumber();
    		var y2 = (_coords[idx] * sin + _coords[idy] * cos + offSetY).toNumber();
    
    		idx = idy++;
    		idy++;
    		var x3 = (_coords[idx] * cos - _coords[idy] * sin + offSetX).toNumber();
    		var y3 = (_coords[idx] * sin + _coords[idy] * cos + offSetY).toNumber();
    
            return [[x0, y0], [x1, y1], [x2, y2], [x3, y3]];
        }
  • Everything on this watch face except the bitmap in the middle and the date is a polygon.  This is a Fenix 7x but it runs on my Forerunner 235 also, just without the bitmap. If you are going to do a lot of polygons, do your design and layout in polar coordinates instead of raster. You have the angle right, starting at 12 and going clockwise. Assume a unit circle, radius from 0 to 1. Always use floats and round, toNumber() truncates.

    	// drawXXXX
    	//		  rMajor is distance of center of polygon from center of display, usually between 0.80 - 0.96
    	//		  aMajor is angle of center of polygon from center of display
    	//		  rMinor is relative size of polygon, a scaling factor, usually 1.0 - rMajor
    	//		  aMinor is rotation angle of polygon,
    	//				 set equal to aMajor to align to watch edge
    	//				 set to 0 to always be vertical, etc.
    	//		  some may have additional parameters
    
    	public function drawRegPoly(rMajor, aMajor, rMinor, aMinor, sides) {
    		var orbRA = new [0] as Array< Array<Float> >;
    		var a1 = self.PIx2 / sides;
    		for (var i = 0; i < sides; i++) {
    			orbRA.add(self.polarAdd(rMajor, aMajor, rMinor, aMinor + a1 * i));
    		}
    		return self.polarDC(orbRA) as Array< Array<Float> >;
    	}
    

    The only functions you will need to develop a whole range of polygons is one to add two vectors, and one to convert the set of polar coordinates to a raster array scaled for the DC for fillPolygon. Then you can reposition, resize and rotate easily just by changing a few parameters.

  • One thing to consider is not "how" you calculate this, but when.

    For the hour and minute hand, you only have to do this when the minute changes, not each time onUpdate() is called. Second hand is different, and it depends on if you use onPartialUpdate()

    Have you looked at the analog sample in the SDK?

  • For the second hand it will also help to calculate the bounding box for clipping at the same time as you are generating the coordinates, this will cut execute time and code size.

  • Good one and thanks for even writing the entire code! This makes the function in the order of 15-20% faster (based on profiler measurements for fenix6xpro). Improvements like this are actually quite mechanical, hopefully the optimizer can such things in the not too distant future..

    idx = idy++; doesn't compile. I'm considering reporting a bug for the compiler.

  • That's an interesting suggestion, although I have no idea how many times onUpdate() is called in real life, when the minute doesn't change, i.e., what impact this would actually make in practice. In this exercise, I was focusing on onPartialUpdate(), as I have quite a clunky second hand, which is also shown in low-power mode.

    Yes, the Analog sample in the SDK is what I started with originally. Looking at it now, I feel it's not particularly well optimized for performance. It is just a sample, so that was probably not the goal anyway, as that often makes the code more confusing.

  • idx = idy++; doesn't compile. I'm considering reporting a bug for the compiler.

    My educated guess is that preincrement (++x) and postincrement (x++) are purposely disallowed as part of larger expressions (like "y = x++"), in order to avoid undefined behavior and to prevent developer confusion. OTOH, "x++;" and "++x;" are fine as standalone statements.

  • When a WF is in low power mode, onUpdate() is called once a minute.  When in high power mode, once a second.  When the watch face goes to high power, that lasts for 10 seconds or so.

    With onEnterSleep() and onExitSleep() you can tell what mode you are in.

  • Yes, the ~10 onUpdate() calls in high power mode plus in my case also calls from onEnterSleep() and onExitSleep() via WatchUi.requestUpdate() are the ones where one could avoid drawing the hour and minute hands (if the minute didn't change). What I was trying to say is that I have no good feel for how often the watch face actually wakes up on average in a real-life scenario.

    I've also been looking at other ways of saving battery, by not drawing the second hand during sleep mode, when the user presumably doesn't look at the watch for an extended time period. But I have yet to find a way to tell if sleep mode is on.

  • You want to redraw everything each time onUpdate() is called.Updating part of the screen is only valid if you are using onPartialUpdate()  Different devices can work differently than the sim, where the dc is cleared prior to onUpdate being called.

    You only need to calculate the hand positions only if the minute changes, and other times, use what you already had calculated.

    Consider the 10 (once per second) calls in high power mode.  At most, you have to calculate the hands position one time vs 10 times.

    Something similar is sunrise/set.  You really only need to calculate those one time per day and then just display what you calculated each time you update the display