Optimization Guidelines

Hi everyone,
First things first: I'm new to MonkeyC, but on good terms with java and been writing for Android + Wear for some time so presumably I know the drill.
Decided to design my own full screen data field and made it run nice on simulator, but when I install it on my Forerunner 230, data field seems to lack performance during actual usage: at least timer field, which I render as two to three
separate strings depending on elapsed time value is rather quirky and most of the time seconds jump around as if there's a half-second delay, then field is redrawn several times in quick succession to make sure data is actual.

I understand that global questions like "why my code runs slow" cannot be answered, thus I ask another vague one: are there specific optimization guidelines/best practices similar to Android Wear ones such as "always prefer object property usage over variable declaration", "clearing dc every onUpdate takes much processing time" etc?

Right now my prime suspects are full screen actions like dc.clear and dc.fillRectangle, but who knows.
If someone is willing to check my code (a little bit cut to fit message limit), then it is provided below.
Sorry for the large plain text, but emulator reports increased memory footprint if I declare a lot of functions.
  • code itself

    ...
    //! The given info object contains all the current workout
    //! information. Calculate a value and save it locally in this method.
    function compute(info) {
    // See Activity.Info in the documentation for available information.
    mTimer = 0;
    mMilestone = "5k";
    mMilestoneTime = 0;
    mPace = 0.0;
    mDistance = 0;
    mHeartRate = 0;
    mCadence = 0;

    if (mAltitude != 0) {
    mAltitudePrev = mAltitude;
    }
    mAltitude = 0;

    if (info != null) {
    if (info.currentHeartRate != null && info.currentHeartRate > 0) {
    mHeartRate = info.currentHeartRate;
    }

    if (info.altitude != null && info.altitude > 0) {
    mAltitude = info.altitude.toLong();
    }

    if (info.currentCadence != null && info.currentCadence > 0) {
    mCadence = info.currentCadence;
    }

    if (info.elapsedTime != null && info.elapsedTime > 0) {
    mTimer = info.elapsedTime / 1000;
    // TODO remove in release
    //mTimer += 30 * 3600;
    }

    if (info.elapsedDistance != null && info.elapsedDistance > 0) {
    mDistance = info.elapsedDistance.toLong();
    var speed = 0;
    if (info.averageSpeed != null && info.averageSpeed > 0) {
    speed = info.averageSpeed;
    } else if (info.currentSpeed != null && info.currentSpeed > 0) {
    speed = info.currentSpeed;
    }

    var targetDistance = 5000;
    if (info.elapsedDistance > 5000 && info.elapsedDistance <= 10000) {
    targetDistance = 10000;
    mMilestone = "10k";
    } else if (info.elapsedDistance > 10000 && info.elapsedDistance <= 21000) {
    targetDistance = 21000;
    mMilestone = "21k";
    } else if (info.elapsedDistance > 21000) {
    targetDistance = 42000;
    mMilestone = "42k";
    }
    if (speed > 0) {
    mMilestoneTime = ((targetDistance - info.elapsedDistance) / speed).toLong();
    mPace = 60 / (speed * 3.6);
    }
    }
    }

    mBattery = System.getSystemStats().battery.toLong();
    mGpsAccuracy = 0;
    if (info.currentLocationAccuracy != null) {
    mGpsAccuracy = info.currentLocationAccuracy;
    }
    }

    //! Display the value you computed here. This will be called
    //! once a second when the data field is visible.
    function onUpdate(dc) {
    // Set the background color
    var bgColor = getBackgroundColor();
    var fgColor = bgColor == Gfx.COLOR_BLACK ? Gfx.COLOR_WHITE : Gfx.COLOR_BLACK;
    var width = dc.getWidth();

    // Set the foreground color and value
    dc.setColor(Gfx.COLOR_TRANSPARENT, bgColor);
    dc.clear();

    drawBatteryIndicator(dc, fgColor);
    drawGpsIndicator(dc, fgColor, bgColor);

    dc.setColor(bgColor, bgColor);
    dc.fillRectangle(48, 0, width - 96, dc.getHeight());
    dc.fillRectangle(0, 38, width, dc.getHeight());

    dc.setColor(fgColor, Gfx.COLOR_TRANSPARENT);
    // timer
    if (mTimer / 60 > 59) {
    // smaller hours : minutes : seconds display
    dc.drawText(width / 2 - 40, -7, Gfx.FONT_NUMBER_MEDIUM, (mTimer / 3600).format("%02d"), Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 2, -7, Gfx.FONT_NUMBER_MEDIUM, ((mTimer / 60) % 60).format("%02d"), Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 2 + 41, -7, Gfx.FONT_NUMBER_MEDIUM, (mTimer % 60).format("%02d"), Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 2 - 21, 0, Gfx.FONT_LARGE, ":", Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 2 + 20, 0, Gfx.FONT_LARGE, ":", Gfx.TEXT_JUSTIFY_CENTER);
    } else {
    // large minutes : seconds display
    dc.drawText(width / 2 - 4, -7, Gfx.FONT_NUMBER_MEDIUM, (mTimer / 60).format("%02d"), Gfx.TEXT_JUSTIFY_RIGHT);
    dc.drawText(width / 2 + 5, -7, Gfx.FONT_NUMBER_MEDIUM, (mTimer % 60).format("%02d"), Gfx.TEXT_JUSTIFY_LEFT);
    dc.drawText(width / 2, 0, Gfx.FONT_LARGE, ":", Gfx.TEXT_JUSTIFY_CENTER);
    }

    // finish time left
    dc.drawText(width / 4 - 5 , 46, Gfx.FONT_NUMBER_MEDIUM, (mMilestoneTime / 60).format("%02d"), Gfx.TEXT_JUSTIFY_RIGHT);
    dc.drawText(width / 4, 53, Gfx.FONT_LARGE, ":", Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 4 + 5, 46, Gfx.FONT_NUMBER_MEDIUM, (mMilestoneTime % 60).format("%02d"), Gfx.TEXT_JUSTIFY_LEFT);

    // distance
    var distStr = Lang.format("$1$.$2$", [(mDistance / 1000).toLong().format("%02d"), ((mDistance % 1000) / 10).toLong().format("%02d")]);
    dc.drawText(width * 3 / 4, 45, Gfx.FONT_NUMBER_MEDIUM, distStr, Gfx.TEXT_JUSTIFY_CENTER);

    // cadence
    var cadenceColor = Gfx.COLOR_PURPLE;
    if (mCadence >= 174 && mCadence <= 183) {
    cadenceColor = Gfx.COLOR_BLUE;
    } else if (mCadence >= 164 && mCadence <= 173) {
    cadenceColor = Gfx.COLOR_DK_GREEN;
    } else if (mCadence >= 153 && mCadence <= 163) {
    cadenceColor = Gfx.COLOR_ORANGE;
    } else if (mCadence < 153) {
    cadenceColor = Gfx.COLOR_RED;
    }
    dc.setColor(cadenceColor, Gfx.COLOR_TRANSPARENT);
    dc.drawText(width / 2 + 12, 137, Gfx.FONT_NUMBER_MEDIUM, mCadence.toString(), Gfx.TEXT_JUSTIFY_LEFT);
    dc.setColor(fgColor, Gfx.COLOR_TRANSPARENT);
    dc.drawText(width / 2 + 6, 138, Gfx.FONT_SMALL, "C", Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 2 + 6, 150, Gfx.FONT_SMALL, "A", Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 2 + 6, 163, Gfx.FONT_SMALL, "D", Gfx.TEXT_JUSTIFY_CENTER);

    // pace
    var paceInt = mPace.toLong();
    var paceDec = ((mPace - paceInt) * 60).toLong();
    var paceStr = Lang.format("$1$.$2$", [paceInt.format("%02d"), paceDec.format("%02d")]);
    dc.drawText(width * 3 / 4, 98, Gfx.FONT_NUMBER_MEDIUM, paceStr, Gfx.TEXT_JUSTIFY_CENTER);

    // altitude
    if (mAltitudePrev != 0) {
    if (mAltitudePrev < mAltitude) {
    dc.setColor(Gfx.COLOR_DK_RED, Gfx.COLOR_TRANSPARENT);
    } else if (mAltitudePrev > mAltitude) {
    dc.setColor(Gfx.COLOR_DK_BLUE, Gfx.COLOR_TRANSPARENT);
    }
    }
    if (mAltitude < 1000) {
    dc.drawText(width / 2 - 14, 137, Gfx.FONT_NUMBER_MEDIUM, mAltitude.toLong().toString(), Gfx.TEXT_JUSTIFY_RIGHT);
    } else {
    dc.drawText(width / 2 - 14, 137, Gfx.FONT_LARGE, mAltitude.toLong().toString(), Gfx.TEXT_JUSTIFY_RIGHT);
    }
    dc.setColor(fgColor, Gfx.COLOR_TRANSPARENT);
    dc.drawText(width / 2 - 6, 138, Gfx.FONT_SMALL, "A", Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 2 - 6, 150, Gfx.FONT_SMALL, "L", Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 2 - 6, 163, Gfx.FONT_SMALL, "T", Gfx.TEXT_JUSTIFY_CENTER);

    // heart rate and percent
    dc.drawText(width / 2 - 14, 98, Gfx.FONT_NUMBER_MEDIUM, mHeartRate.toString(), Gfx.TEXT_JUSTIFY_RIGHT);

    var hrPerc = (mHeartRate * 100 / 184).toLong();
    var hrPercColor = Gfx.COLOR_YELLOW;
    if (hrPerc >= 90) {
    hrPercColor = Gfx.COLOR_RED;
    } else if (hrPerc >= 80) {
    hrPercColor = Gfx.COLOR_ORANGE;
    } else if (hrPerc >= 50) {
    hrPercColor = Gfx.COLOR_DK_GREEN;
    }
    dc.setColor(hrPercColor, Gfx.COLOR_TRANSPARENT);
    dc.drawText(12, 112, Gfx.FONT_LARGE, hrPerc.toString(), Gfx.TEXT_JUSTIFY_LEFT);
    dc.setColor(fgColor, Gfx.COLOR_TRANSPARENT);

    // labels
    dc.setColor(fgColor, Gfx.COLOR_TRANSPARENT);
    dc.drawText(width / 4, 35, Gfx.FONT_SMALL, labelFinish +" "+ mMilestone, Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width * 3 / 4, 35, Gfx.FONT_SMALL, labelDistance, Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width * 3 / 4, 87, Gfx.FONT_SMALL, labelPace, Gfx.TEXT_JUSTIFY_CENTER);
    dc.drawText(width / 4, 87, Gfx.FONT_SMALL, labelHeartRate, Gfx.TEXT_JUSTIFY_CENTER);

    // general split lines
    dc.setColor(Gfx.COLOR_BLUE, Gfx.COLOR_TRANSPARENT);
    dc.setPenWidth(2);
    dc.drawLine(0, 38, width, 38);
    dc.drawLine(48, 0, 48, 38);
    dc.drawLine(width - 48, 0, width - 48, 38);

    dc.drawLine(width / 2, 38, width / 2, 180);
    dc.drawLine(0, 90, width, 90);
    dc.drawLine(0, 142, width, 142);
    }

    function drawBatteryIndicator(dc, fgColor) {
    // battery
    var batteryColor;
    var centerX = dc.getWidth() / 2;
    var centerY = dc.getHeight() / 2;
    if (mBattery >= 75) {
    batteryColor = Gfx.COLOR_DK_GREEN; //(255 - 105 * (mBattery - 75) / 25) * 256;
    } else if (mBattery >= 40) {
    batteryColor = Gfx.COLOR_GREEN;
    } else if (mBattery >= 30) {
    batteryColor = Gfx.COLOR_YELLOW;
    } else if (mBattery >= 15) {
    batteryColor = Gfx.COLOR_ORANGE;
    } else {
    batteryColor = Gfx.COLOR_RED;
    }
    dc.setColor(batteryColor, Gfx.COLOR_TRANSPARENT);
    dc.fillCircle(centerX, centerY, centerX - 25 + mBattery * 0.28f);

    dc.setColor(fgColor, Gfx.COLOR_TRANSPARENT);
    dc.drawText(47, 11, Gfx.FONT_NUMBER_MILD, mBattery.toString(), Gfx.TEXT_JUSTIFY_RIGHT);
    }

    function drawGpsIndicator(dc, fgColor, bgColor) {
    // gps indicator
    var width = dc.getWidth();
    //var gpsColor = Gfx.COLOR_WHITE;
    var gpsColor = bgColor;
    if (mGpsAccuracy == 4) {
    gpsColor = Gfx.COLOR_DK_GREEN;
    } else if (mGpsAccuracy == 3) {
    gpsColor = Gfx.COLOR_GREEN;
    } else if (mGpsAccuracy == 2) {
    gpsColor = Gfx.COLOR_ORANGE;
    } else if (mGpsAccuracy == 1) {
    gpsColor = Gfx.COLOR_RED;
    }

    dc.setColor(bgColor, bgColor);
    dc.fillRectangle(width - 48, 0, 48, 38);
    dc.setColor(gpsColor, bgColor);

    var stepWidth = 7;
    dc.setPenWidth(5);

    for (var radius = width / 2 - 28; radius <= (width / 2 - stepWidth * (4 - mGpsAccuracy)); radius += stepWidth) {
    dc.drawArc(width / 2, dc.getHeight() / 2, radius, Gfx.ARC_CLOCKWISE, 56, 28);
    }
    }
    }
  • Definitely one thing I can recommend:

    Do not repeat certain computations like
    width / 2

    Rather do this:

    var center = width / 2;
  • Definitely one thing I can recommend:

    Do not repeat certain computations like
    width / 2

    Rather do this:

    var center = width / 2;

    Thank you for the tip, but that didn't help much: still running good in emulator but stuttering on the watch.
    So if there are no profile tools (and I didn't find any clarification on that topic in either programmers guide or developer documentation), then I'll try to do this plain old way: sequentially remove blocks of code and check how does this affect device performance.

    Considering how current FR230 firmware treats manually installed data fields, this can be painful process: sometimes when I try to set up field, which I loaded directly to watch, data screen setup shows 3 to 5 of 2 fields used despite no other IQ data fields being used or downloaded to device.

    But still, if someone knows the performance tricks, please, share.
    Thanks in advance.
  • If it could help speed up things for you, try removing this code first and check the performance.

    I know drawing arcs is a costly operation. And then you do it in a for loop to begin with...

    for (var radius = width / 2 - 28; radius <= (width / 2 - stepWidth * (4 - mGpsAccuracy)); radius += stepWidth) {
    dc.drawArc(width / 2, dc.getHeight() / 2, radius, Gfx.ARC_CLOCKWISE, 56, 28);
    }
  • If it could help speed up things for you, try removing this code first and check the performance.

    I know drawing arcs is a costly operation. And then you do it in a for loop to begin with...

    for (var radius = width / 2 - 28; radius <= (width / 2 - stepWidth * (4 - mGpsAccuracy)); radius += stepWidth) {
    dc.drawArc(width / 2, dc.getHeight() / 2, radius, Gfx.ARC_CLOCKWISE, 56, 28);
    }

    Thanks for the info, will certainly try that.
    But as you could guess reading this code, I wanted to impement gps indicator, which consists of several coned arc segments where amount of segments depends on gps accuracy. Maybe if this part is marked as culprit during on-device tests, I'll try to let it slide with a single fillRectangle. Especially considering that during normal activity this doesn't change often and if the operation is CPU intensive, this will also eat battery.

    Too bad we can't prerender immutable parts as a bitmap and use it to draw with a single operation during onUpdate 8((
  • Couple of things you could do to display an arc, without actually drawing one for something like the GPS accuracy where there are 5 specific levels:

    1) use a bitmap for each of the levels

    2) a custom font where you map characters to a image in that font - for GPS you could use "0" to "4", for example, and then just do a toString() on the value you get and then you have the image for that character displayed by just using that font. With a custom font, you can also easily change the color, without having different bitmaps. Many things where you see icons for different things (like bluetooth status) use a custom "icon font", for example.

    dc.drawText(x,y,arcfont,"4",Gfx.TEXT_JUSTIFY_CENTER);

    would draw the arc for "QUALITY_GOOD" for instance.
  • If you want to know which portions of your code are taking a long time to execute on hardware, I recommend timing your code. You can use Toybox.System.getTimer() to read the free-running millisecond timer on the device. By reading this timer at various points in your code, you can compute the execution time of each section. The call to the timer shouldn't have much of an impact on the timing, but printing out any results will, so make sure you collect all your timings before doing any println() calls.

    Once you know which sections are slow, you can focus on figuring out ways to speed them up.
  • I'm going to capture some of these thoughts and add them to the FAQ.
  • Thanks everyone, below are the results of my little research.
    Replacing the arcs with filled squares helped a little, but still timer was twitching at times during several minutes observation period. Thus I decided to also create pace and distance display values only when the visible values has changed, this helped another little bit. Most visible impact though I've found when changed battery read interval from second to minute. Don't know that's just a coincidence or System.getSystemStats() really takes noticeable time to execute.
    Right now data field is working rather smooth and I'll take some time to field-test it during actual running for a couple of weeks to find out most obvious bugs.
    Meanwhile, since the performance topic became my obsession recently, I'll continue to run simulator tests with a method offered by Brian.ConnectIQ, but since there is no inbuilt performance tool it'll take some time to analyse.

    Also I'd like to give personal thanks jim_m_58 for the tip on how to implement symbols: I've already seen this technique discussed on forum, but wasn't sure that this is the common practice. Also didn't know that color could be changed without using another set of images, it's nice that bitmap tinting is present in some way, gives a lot of ways for personalization. Still, there's another bottleneck: while data field has only 16kb available memory, using bitmap font with alpha channel may dramatically increase memory footprint.
  • optimisaion is both a hit and miss and plenty of coffee and tries.
    Getting memory usage down is a BIG BIG thing and every little bit helps.
    Things like shortening variable names (eg PACE vs p)

    for the screen updates, FPS if I remember is like 1sec and you need the updates etc to finish in approx 500ms for no twitching to occur.
    Littering plenty of println and systimers helps.

    also helps is to actually put these little systimers on the display itself to determine where is eating up stuffs.
    Complex DataFields like mine takes a LOT of optimisation.

    Good Luck.