Out Of Memory Error on dictionary declaration

While working on my very first Garmin project I'm getting this error Error: Out Of Memory Error
Details: Failed invoking <symbol>

I think this happens because I'm trying to declare a dictionary from an extreamly long string. It has more than 5500 characters. I generated this string from a json file using python because I want to make my data field run on a forerunner 235 and I found out that this device can load resources because of the limited api level. So I decided to just generate an .mc file with the array in it. All looks good though.

Why can't I create a dictionary like this? Is there an other way to get this data into my app? 

  • You could try defining the string data as a property and loading it that way.

    The problem is if you define the string data in code, you will use at least twice the amount of memory needed for the string:

    - the memory for the code to construct the string

    - the memory for string itself

    In general, dictionaries are very expensive, too.

    If you must declare big data structures in code, it can sometimes help to wrap the declarations in a function. That way, the data will only exist when you need it (which may not be all the time.) That won't help in this case.

  • Could you give an example on how to use a property for this? I'm not the most experienced coder and I'm not familiar with properties yet. 

    This is what my data looks like: 

    "LOCATION1" => [51.4338707, 4.9975666534], "LOCATION2" => [51.4323701175, 5.032898329], etc...

    I have 114 items I would like to add. I need the name and their location coordinates.

  • Hmm, that's a lot of data. There's a lot of overhead here with the dictionary and the array for each value. (Each array has an overhead of about 8 bytes IIRC.)

    How will this data be used?

    Would it be possible to flatten the data like this?

    [

       "LOCATION1",

       51.4338707,

       4.9975666534,

       "LOCATION2",

       ...

    ]

    If you're worried about efficient lookups, you could still have a dictionary with the location names, but each value would be an index into the array.

    e.g.

    { "LOCATION1" => 0, "LOCATION2" => 2, "LOCATION3" => 4, ... }

    [ 51.4338707, 4.9975666534, 51.4323701175, 5.032898329, ... ]

  • If you wanted to put this information in a property, you could encode it as one really long string.

    e.g.

    "LOCATION1,51.4338707,4.9975666534,LOCATION2,..."

    And write code to parse the string and create the data structures you want. So you'd at least save memory for the code that contains the data.

    But then the parsing code itself would take up memory. (Probably less memory than the code for 114 items themselves.)

    I have used this technique when I wanted to store lots of static data in an app that runs on old devices. But then the code to actually read and parse the data can slow the app down at init time. If your app takes too long to init, it will actually be terminated with a "watchdog timeout error".

    So the only way to know if this approach will work for you is to try it out.

  • Your right. Using a flat array is much more effective than a multi-dimensional array or a dictionary.  I do this in a number of apps

  • I now created a string like this: "LOCATION1,51.4338707,4.9975666534,LOCATION2,..." 
    Should I make a property of this string or can I just parse the string into and array? What is the most efficient way of doing this?  
    I'm wondering how to parse this. I don't see any parse functionality in the scripting reference so tips on this are welcome too.

  • The only point of having it in a string like that is to make it a property, so the data itself doesn't have to be in code (which wastes memory).

    If you were going to put the string in code, you may as well just put the array in the code in the first place, because there's little benefit in converting the data if the data is already in your code.

    First I would just try your app with the array declaration in the code. If you don't run out of memory, then you're good.

    If you need to free more memory, remove the array declaration from code, add the string as a property resource and create the code to parse the string.

    Monkey C doesn't have a lot of utility functions for parsing strings (e.g. there's nothing like split() from javascript), so you would have to write your own stuff from scratch.

    In your case you'll need to:

    - split the string into an array of strings, based on the comma separator

    - convert all the numerical strings to floats

    I don't have time to write example code rn, but you can start by looking at the String class. The find and substring functions will be helpful:

    developer.garmin.com/.../String.html

  • Here's an old thread with example code for splitting strings:

    [https://forums.garmin.com/developer/connect-iq/f/discussion/3737/stringtokenizer/25513#25513]

    Once you have your array of strings, you can convert all the numerical strings to floats with String.toFloat():

    [https://developer.garmin.com/connect-iq/api-docs/Toybox/Lang/String.html#toFloat-instance_function]

  • That is enough to keep me busy for some time. 

    Are you talking about these properties? https://developer.garmin.com/connect-iq/api-docs/Toybox/Application/Properties.html 
    The reference states its API Level 2.4.0 ? I'm still not really sure what they are and where they live and how to use them :D . Should I add the string using setValue?

    Edit: I've tried the split code above and it works but if I feed in my flattened string I get an error again and if is shorten it, it does work so I guess even just using the flat string is a problem. 

  • Edit: I've tried the split code above and it works but if I feed in my flattened string I get an error again and if is shorten it, it does work so I guess even just using the flat string is a problem. 

    I think you'd have to work on the function to make sure it works properly. There's no reason in principle that it shouldn't work.

    I mean "property" as in "app settings", except in this case you won't have a corresponding setting.

    What you linked to is the new API for accessing app properties. On old devices you would use AppBase.getProperty() and AppBase.setProperty():

    [https://developer.garmin.com/connect-iq/core-topics/persisting-data/#accessingpropertiesandstorageapplicationproperties]

    https://developer.garmin.com/connect-iq/core-topics/app-settings/

    You can define a property as a default value and not define an associated setting but you cannot define a setting without tying it to a property.

    So you just want to have a property with a default value (equal to your long string), and you can read it programmatically. That way you don't have to waste memory in your code for the data (since the data will live in your app resources.)

    For some reason I don't see any examples of the <properties> xml tags in Garmin's official docs, but here's one for you:

    <properties>
      <property id="locations" type="string">Location1,12.34,45.67,Location2,...</property>
    </properties>

    Note that longer property IDs take more memory than shorter property IDs (because all of your properties/resources get loaded into a dictionary at run-time). The actual value of your properties does not take run-time memory (until you load one of them.)

    So you would want something like

    var locationsString = myApp.getProperty("locations");

    var locations = parseLocations(locationsString); // translate the string to the real data (you have to implement parseLocations())

    locationsString = null; // explicitly free memory for the string (alternatively, it will be implicitly freed once locationsString is out of scope)

    EDIT: Sorry, this was bad advice. I should've said to use a string resource, not a property, to encode the data that you want to store.

    e.g. strings.xml:
    <strings>
        <string id="locationData">Location1,1.234,2.3456,Location2,...</string>
    </strings>

    Code:

    var locationDataString = WatchUi.loadResource(Rez.Strings.locationData);

    var locationData = parseLocationData(locationDataString); // you have to write parseLocationData()

    EDIT: This is just not great advice in general. In most cases, the best thing to do (if JSON resources aren't available), is to just define the data in the proper format in your app.