How can I use conditional code and keep the code size minimal?

In my monkey.jungle I set either annotation foo or no_foo for each device.

In my code I have:

(:foo) const FOO = true;
(:no_foo) const FOO = false;

And I use it extensively like:

if (FOO) {
  // do stuff I only need when foo is enabled
}

This works nice with SDK 7+ because it eliminates the whole code block when building for devices with foo disabled.

However there are still a couple of situations when this doesn't work or not as nice. For example if I have a function:

class Example {
  (:foo) function fooOnly() as Void {...}

  function other() as Void {
    if (FOO) {
      fooOnly();
    }
  }
}

This gives a compile time error: Undefined symbol ':fooOnly' detected.

There is a not too nice work around: declaring fooOnly() as an empty function.
(:no_foo) function fooOnly() as Void {}
but I don't like this, because this does increase the code size for the devices that have the least amount of memory (and thus I disable foo).

It can be even better if one uses Prettier Monkey C:
(:no_foo, :inline) function fooOnly() as Void {}
but I don't use Prettier optimizer in all my apps, so I'm looking for another ideas.

Another example is when a class variable is only needed for a certain feature:

class Example {
  (:foo) var isFooActive as Boolean = false; // it's set to true somewhere in the class
  function other() as Void {
    if (FOO) {
      if (isFooActive) {...}
    }
  }
}

This will also not compile, because the compiler doesn't know isFooActive, so I have to add a dummy variable:
(:no_foo) isFooActive = false;
Which kind of makes it useless to use the annotation :)

Also for bigger objects, like Fields in a fit contributor, it does help a bit, but I still need a dummy nullable version:

class Example {
  (:foo) var fooField as Field;
  (:no_foo) var fooField as Field?; // hack
  function initialize() {
    if (FOO) {
      fooField = session.createField(...);
    }
  }
  function compute() as Void {
    if (FOO) {
      var fooValue = computeFoo();
      (fooField as Field).setData(fooValue);
    }
  }
}

  • personally I'm not a fan of exludeannotations as it really can litter up your code, especially when you start to have many of them.

    I do use them on occasion but another problem with them is that I also always have to wrap my head around the fact that it are exludeannotations and not includeannotations....

    I think when you want a more structural approach for different code paths then the path is to use a jungle in combination with class inheritance. I wrote an article about this during the previous decade and I think the principle still stands.  starttorun.info/.../

  • I agree 100%.  I rarely use excludes.  I will pull in completely different MC files at times, like commonView.mc and semitoctagonView.mc, so the code is clearer and easier to understand.

  • I think your solution solves a different problem. My main goal is to optimize for memory size, to be able to squeeze as many small features for devices with smaller memory size as possible. I already gave up on code readability :( 

    Your solution looks to me more like a structured way of organizing the code (better readability), but at a price of a small memory increase (class inheritence: as far as I remember if I have a class: A, and I split it to AParent and AChild, and then useAChild, then the build that uses AChild will use more memory than the build that uses A)

  • your solutions IMHO also use some duplicate code. How do you do this:

    function a() {
      neededEveryWhere();
      lowMem() or highMem();
      onlyNeededForTouch();
      neededForAll();
      onlyNeededForColorDisplay();
      either color() or monochrome();
      commonCode();
      etc...
    }

    Either you'll have a significant portion of your code duplicated, or you'll have many small functions all over that call each other and pass lots of variables (max 9 of course) or your apps are significantly simpler than mine (or better written?)

  • Use the C preprocessor so you can include/exclude code on a line-by-line basis Grimacing (I don't claim this is a good practice from a maintainability or readability standpoint, or from any pov other than being able to maximize memory savings by compiling the least amount of code possible *)

    Here's a thread with my janky script I used to use in Eclipse:

    https://forums.garmin.com/developer/connect-iq/f/discussion/7733/solution-for-line-by-line-mc-conditional-compilation

    You (flocsy) *might* like it bc it specifies annotations to be associated with predefined build targets in an *inclusive* manner, at the cost of requiring you to do some extra housekeeping for global symbols, whether annotated or not.

    (However, excludeAnnotations directives still need to be set up in the usual way in your jungle file).

    The most recent post in that thread has a link to a preprocessor built in node.js - I haven't tried, but it's probably nicer than my script.

    EDIT: the major disadvantage with this type of approach is it will obviously completely break all the built-in language features in the IDE, except for the case of the auto-genned source which will actually be built.

    [*] ofc if you are really serious about saving memory in code size (and even data size) on old devices, you will be applying all kinds of anti-patterns that the optimizer wouldn't automatically do for you, like:

    - avoiding classes and dictionaries wherever possible:

    -- e.g. instead of returning an instance of a class which contains handful of values, return an array with the same values [at least Monkey C supports tuples now, so such a scheme can actually be type-safe]

    -- e.g. instead of writing a class to implement a stack (for example), implement your stack using a handful of global functions which return and accept the "class state" as a lightweight object (e.g. an array of values)

    -- avoid using exceptions but just have a global variable which signifies that an error happened

    The big memory saving anti-pattern that the optimizer does do for you is replace enums with their values [ofc it's ok when the optimizer does it, as the enums are still there in source code.]

    You might also do some things which aren't necessarily anti-patterns, like:

    - avoid the use of switch statements in favour of if-else statements

    - avoid the use of if-else statements in favour of array look ups, when possible

    - bitpack static integer data when possible

    (e.g. if you have lots of static data in the form of ints smaller than 32-bits, you can probably save a lot of memory by packing that data into arrays of 32-bit ints. A related strategy would be to encode static data that would normally a boolean as an individual bit.)

  • It depends on how you write the code to begin with,  Both PererD and I have been doing this for a long time. 

    I can think a few cases where I've completely re-written some of my apps that were done with CIQ 1.0.0, when there were a tiny number of devices to support.

    There is a learning curve with CIQ, and you'll probably find you can write much tighter and efficient code today than when you started, and sometimes a re-write makes more sense than adding stuff like excludes all over the place.

    With most of my apps today, adding a new target is little more than adding the new device and doing some testing.

    Two exceptions are devices with onboard maps, where I have to add 2 lines to my jungle file to pull in different code and bitmaps for the "map" screen, and complication publishers on devices with complications to pull in a related xml file needed to publish.

    for things like color vs B&W, I use different files for settings, so on color devices, I can pick specific colors, while on B&W, devices, I allow for a B or W background and set the text colors to be the opposite 

  • Yes class inheritance does come with a memory cost.

    I don't like copy/paste at all, so i'm not doing that.

    It's ultimately your choice what's your main purpose

    I also have apps that are tight in memory, but I've optimized in other ways to be able to retain the inheritance pattern (multiple levels of inheritance actually,  you're not limited to 1 subclass, as is also explained in my blog post).