RAM leak in my Watchface

Posted on
  • I am kind of struggeling with RAM issues. The watchface claims low ram after a while. Is there somebody who can check the code for the leak?
    https://github.com/espruino/BangleApps/b­lob/master/apps/hworldclock/app.js
    I checked with

    process.memory(false);
    

    So my guess is that the status change from "show every 10 seconds" to "show every second" causes RAM usage. So my theory is that I use and discard the setInterval in a wrong way. Any hints on that?

  • I'm just trying this here and I don't actually see any increase in usage...

    What if you run process.memory().usage? By specifying false as an argument you're telling Espruino not to garbage collect, so the usage will tend to go up over time anyway - but that's not a big deal as when garbage collection runs it goes back down.

    If you do just process.memory() then a GC pass is done first, so you're always looking at the amount of memory your code is really using.

  • Thanks for the explanation!
    This is my test code:

    // ------- Settings file
    const SETTINGSFILE = "hworldclock.json";
    var secondsMode;
    var showSunInfo;
    var colorWhenDark;
    // ------- Settings file
    
    const big = g.getWidth()>200;
    // Font for primary time and date
    const primaryTimeFontSize = big?6:5;
    const primaryDateFontSize = big?3:2;
    require("Font5x9Numeric7Seg").add(Graphi­cs);
    require("FontTeletext10x18Ascii").add(Gr­aphics);
    
    // Font for single secondary time
    const secondaryTimeFontSize = 4; 
    const secondaryTimeZoneFontSize = 2;
    
    // Font / columns for multiple secondary times
    const secondaryRowColFontSize = 2;
    const xcol1 = 10;
    const xcol2 = g.getWidth() - xcol1;
    
    const font = "6x8";
    
    /* TODO: we could totally use 'Layout' here and
    avoid a whole bunch of hard-coded offsets */
    
    const xyCenter = g.getWidth() / 2;
    const xyCenterSeconds = xyCenter + (big ? 85 : 68);
    const yAmPm = xyCenter - (big ? 70 : 48);
    const yposTime = big ? 70 : 55;
    const yposTime2 = yposTime + (big ? 100 : 60);
    const yposDate = big ? 135 : 95;
    const yposWorld = big ? 170 : 120;
    
    const OFFSET_TIME_ZONE = 0;
    const OFFSET_HOURS = 1;
    
    var PosInterval = 0; 
    
    var offsets = require("Storage").readJSON("hworldclock­.settings.json") || [];
    
    //=======Sun
    setting = require("Storage").readJSON("setting.jso­n",1);
    E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ
    SunCalc = require("hsuncalc.js");
    const LOCATION_FILE = "mylocation.json";
    var rise = "read";
    var set	= "...";
    //var pos	 = {altitude: 20, azimuth: 135};
    //var noonpos = {altitude: 37, azimuth: 180};
    //=======Sun
    
    var ampm = "AM";
    
    // TESTING CODE
    // Used to test offset array values during development.
    // Uncomment to override secondary offsets value
    /*
    const mockOffsets = {
    	zeroOffsets: [],
    	oneOffset: [["UTC", 0]],
    	twoOffsets: [
    		["Tokyo", 9],
    		["UTC", 0],
    	],
    	 fourOffsets: [
    		["Tokyo", 9],
    		["UTC", 0],
    		["Denver", -7],
    		["Miami", -5],
    	],
    };*/
    
    
    // Example hworldclock.settings.json
    // [["London","0"],["NY","-5"],["Denver","-­6"]]
    
    
    // Uncomment one at a time to test various offsets array scenarios
    //offsets = mockOffsets.zeroOffsets; // should render nothing below primary time
    //offsets = mockOffsets.oneOffset; // should render larger in two rows
    //offsets = mockOffsets.twoOffsets; // should render two in columns
    //offsets = mockOffsets.fourOffsets; // should render in columns
    
    // END TESTING CODE
     
    
    // Load settings
    function loadMySettings() {
    	// Helper function default setting
    	function def (value, def) {return value !== undefined ? value : def;}
    
    	var settings = require('Storage').readJSON(SETTINGSFILE­, true) || {};
    	secondsMode = def(settings.secondsMode, "when unlocked");
    	showSunInfo = def(settings.showSunInfo, true);
    	colorWhenDark = def(settings.colorWhenDark, "green");
    }
    
    
    // Check settings for what type our clock should be
    var _12hour = (require("Storage").readJSON("setting.js­on",1)||{})["12hour"]||false;
    
    // timeout used to update every minute
    var drawTimeout;
    var drawTimeoutSeconds;
    var secondsTimeout;
    
    g.setBgColor(g.theme.bg);
    
    // schedule a draw for the next minute
    function queueDraw() {
    	if (drawTimeout) clearTimeout(drawTimeout);
    		drawTimeout = setTimeout(function() {
    			drawTimeout = undefined;
    			draw();
    		}, 60000 - (Date.now() % 60000));
    }
    
    // schedule a draw for the next second
    function queueDrawSeconds() {
    	if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
    		drawTimeoutSeconds = setTimeout(function() {
    			drawTimeoutSeconds = undefined;
    			drawSeconds();
    			//console.log("TO: " + secondsTimeout);
    		}, secondsTimeout - (Date.now() % secondsTimeout));
    }
    
    function doublenum(x) {
    	return x < 10 ? "0" + x : "" + x;
    }
    
    function getCurrentTimeFromOffset(dt, offset) {
    	return new Date(dt.getTime() + offset * 60 * 60 * 1000);
    }
    
    function updatePos() {
    
    	coord = require("Storage").readJSON(LOCATION_FIL­E,1)||  {"lat":0,"lon":0,"location":"-"}; //{"lat":53.3,"lon":10.1,"location":"Pat­tensen"};
    	if (coord.lat != 0 && coord.lon != 0) {
    	//pos = SunCalc.getPosition(Date.now(), coord.lat, coord.lon);	
    	times = SunCalc.getTimes(Date.now(), coord.lat, coord.lon);
    	rise = "^" + times.sunrise.toString().split(" ")[4].substr(0,5);
    	set	= "v" + times.sunset.toString().split(" ")[4].substr(0,5);
    	//noonpos = SunCalc.getPosition(times.solarNoon, coord.lat, coord.lon);
    	} else {
    		rise = null;
    		set  = null;
    	}
    }
    
    
    function drawSeconds() {
    	// get date
    	var d = new Date();
    	var da = d.toString().split(" ");
    
    	// default draw styles
    	g.reset().setBgColor(g.theme.bg).setFont­Align(0, 0);
    
    	// draw time
    	var time = da[4].split(":");
    	var seconds = time[2];
    
    	g.setFont("5x9Numeric7Seg",primaryTimeFo­ntSize - 3);
    	if (g.theme.dark) {
    		if (colorWhenDark == "green") {
    			g.setColor("#22ff05");
    		} else {
    			g.setColor(g.theme.fg);
    		}
    	} else {
    		g.setColor(g.theme.fg);
    	}
    	//console.log("---");
    	//console.log(seconds);
    	if (Bangle.isLocked() && secondsMode != "always") seconds = seconds.slice(0, -1) + ':::'; // we use :: as the font does not have an x
    	//console.log(seconds);
    	g.drawString(`${seconds}`, xyCenterSeconds, yposTime+14, true); 
    	queueDrawSeconds();
    
    }
    
    function draw() {
    	// get date
    	var d = new Date();
    	var da = d.toString().split(" ");
    
    	// default draw styles
    	g.reset().setBgColor(g.theme.bg).setFont­Align(0, 0);
    
    	// draw time
    	var time = da[4].split(":");
    	var hours = time[0],
    	minutes = time[1];
    	
    	
    	if (_12hour){
    		//do 12 hour stuff
    		if (hours > 12) {
    			ampm = "PM";
    			hours = hours - 12;	
    			if (hours < 10) hours = doublenum(hours);	
    		} else {
    			ampm = "AM";	 
    		}	 
    	}	
    
    	//g.setFont(font, primaryTimeFontSize);
    	g.setFont("5x9Numeric7Seg",primaryTimeFo­ntSize);
    	if (g.theme.dark) {
    		if (colorWhenDark == "green") {
    			g.setColor("#22ff05");
    		} else {
    			g.setColor(g.theme.fg);
    		}
    	} else {
    		g.setColor(g.theme.fg);
    	}
    	g.drawString(`${hours}:${minutes}`, xyCenter-10, yposTime, true);
    	
    	// am / PM ?
    	if (_12hour){
    	//do 12 hour stuff
    		//var ampm = require("locale").medidian(new Date()); Not working
    		g.setFont("Vector", 17);
    		g.drawString(ampm, xyCenterSeconds, yAmPm, true);
    	}	
    
    	if (secondsMode != "none") drawSeconds(); // To make sure...
    	
    	// draw Day, name of month, Date	
    	//DATE
    	var localDate = require("locale").date(new Date(), 1);
    	localDate = localDate.substring(0, localDate.length - 5);
    	g.setFont("Vector", 17);
    	g.drawString(require("locale").dow(new Date(), 1).toUpperCase() + ", " + localDate, xyCenter, yposDate, true);
    
    	g.setFont(font, primaryDateFontSize);
    	// set gmt to UTC+0
    	var gmt = new Date(d.getTime() + d.getTimezoneOffset() * 60 * 1000);
    
    	// Loop through offset(s) and render
    	offsets.forEach((offset, index) => {
    	dx = getCurrentTimeFromOffset(gmt, offset[OFFSET_HOURS]);
    	hours = doublenum(dx.getHours());
    	minutes = doublenum(dx.getMinutes());
    
    
    	if (offsets.length === 1) {
    		var date = [require("locale").dow(new Date(), 1), require("locale").date(new Date(), 1)];	
    		// For a single secondary timezone, draw it bigger and drop time zone to second line
    		const xOffset = 30;
    		g.setFont(font, secondaryTimeFontSize).drawString(`${hou­rs}:${minutes}`, xyCenter, yposTime2, true);
    		g.setFont(font, secondaryTimeZoneFontSize).drawString(of­fset[OFFSET_TIME_ZONE], xyCenter, yposTime2 + 30, true);
    
    		// draw Day, name of month, Date
    		g.setFont(font, secondaryTimeZoneFontSize).drawString(da­te, xyCenter, yposDate, true);
    	} else if (index < 3) {
    		// For > 1 extra timezones, render as columns / rows
    		g.setFont(font, secondaryRowColFontSize).setFontAlign(-1­, 0);
    		g.drawString(
    			offset[OFFSET_TIME_ZONE],
    			xcol1,
    			yposWorld + index * 15,
    			true
    		);
    		g.setFontAlign(1, 0).drawString(`${hours}:${minutes}`, xcol2, yposWorld + index * 15, true);
    	}
    	});
    
    	if (showSunInfo) {
    		if (rise != null){
    			g.setFontAlign(-1, 0).setFont("Vector",12).drawString(`${ri­se}`, 10, 3 + yposWorld + 3 * 15, true); // draw rise
    			g.setFontAlign(1, 0).drawString(`${set}`, xcol2, 3 + yposWorld + 3 * 15, true); // draw set
    		} else {
    			g.setFontAlign(-1, 0).setFont("Vector",11).drawString("set city in \'my location\' app!", 10, 3 + yposWorld + 3 * 15, true); 
    		}
    	}
    	//debug settings
    	//g.setFontAlign(1, 0);
    	//g.drawString(secondsMode, xcol2, 3 + yposWorld + 3 * 15, true);
    	//g.drawString(showSunInfo, xcol2, 3 + yposWorld + 3 * 15, true);
    	//g.drawString(colorWhenDark, xcol2, 3 + yposWorld + 3 * 15, true);
    
    	queueDraw();
    	
    	if (secondsMode != "none") queueDrawSeconds();
    }
    
    // clean app screen
    g.clear();
    
    // Init the settings of the app
    loadMySettings();
    
    // Show launcher when button pressed
    Bangle.setUI("clock");
    Bangle.loadWidgets();
    Bangle.drawWidgets();
    
    
    // draw immediately at first, queue update
    draw();
    
    
    if (!Bangle.isLocked())  { // Initial state
    		if (showSunInfo) {
    			if (PosInterval != 0) clearInterval(PosInterval);
    			PosInterval = setInterval(updatePos, 60*10E3);	// refesh every 10 mins
    			updatePos();
    		}
    
    		secondsTimeout =  1000;
    		if (secondsMode != "none") {
    			if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
    			drawTimeoutSeconds = undefined;
    		}	
    		if (drawTimeout) clearTimeout(drawTimeout);
    		drawTimeout = undefined;
    		draw(); // draw immediately, queue redraw
    		
      }else{
    		if (secondsMode == "always") secondsTimeout = 1000;
    		if (secondsMode == "when unlocked") secondsTimeout = 10 * 1000;
    		
    		if (secondsMode != "none") {
    			if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
    			drawTimeoutSeconds = undefined;
    		}
    		if (drawTimeout) clearTimeout(drawTimeout);
    		drawTimeout = undefined;
    
    		if (showSunInfo) {
    			if (PosInterval != 0) clearInterval(PosInterval);
    			PosInterval = setInterval(updatePos, 60*60E3);	// refesh every 60 mins
    			updatePos();
    		}
    		draw(); // draw immediately, queue redraw
    		
      }
     
    
    Bangle.on('lock',on=>{
      if (!on) { // UNlocked
     console.log("1 i " + process.memory().usage);
    		if (showSunInfo) {
    			if (PosInterval != 0) clearInterval(PosInterval);
    			PosInterval = setInterval(updatePos, 60*10E3);	// refesh every 10 mins
    			updatePos();
    		}
    
    		secondsTimeout =  1000;
    		if (secondsMode != "none") {
    			if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
    			drawTimeoutSeconds = undefined;
    		}	
    		if (drawTimeout) clearTimeout(drawTimeout);
    		drawTimeout = undefined;
     console.log("1 m " + process.memory().usage);
    
    		draw(); // draw immediately, queue redraw
    		 console.log("1 o " + process.memory().usage);
    
      }else{  // locked
     console.log("2 i " + process.memory().usage);
    
    		if (secondsMode == "always") secondsTimeout = 1000;
    		if (secondsMode == "when unlocked") secondsTimeout = 10 * 1000;
    		
    		if (secondsMode != "none") {
    			if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds);
    			drawTimeoutSeconds = undefined;
    		}
    		if (drawTimeout) clearTimeout(drawTimeout);
    		drawTimeout = undefined;
     console.log("2 m " + process.memory().usage);
    
    		if (showSunInfo) {
    			if (PosInterval != 0) clearInterval(PosInterval);
    			PosInterval = setInterval(updatePos, 60*60E3);	// refesh every 60 mins
    			updatePos();
    		}
    		draw(); // draw immediately, queue redraw		
     console.log("2 o " + process.memory().usage);
      }
     });
    

    I get this:

    2 i 2104
    2 m 2074
    2 o 2104
    
    1 i 2137
    1 m 2107
    1 o 2137
    
    2 i 2170
    2 m 2140
    2 o 2170
    
    1 i 2203
    1 m 2173
    1 o 2203
    
    2 i 2236
    2 m 2206
    2 o 2236
    

    So after each status change (1 vs. 2) more RAM is used somehow.

    When I remove

    Bangle.loadWidgets();
    Bangle.drawWidgets();
    

    it seems to stop loosing RAM. So my next guess is one of the widgets.
    ...working on it...

  • Very interesting. Deleted the Calendar Week widget. Now it is consistent:

    2 i 2005
    2 m 1975
    2 o 2005
    1 i 2005
    1 m 1975
    1 o 2005
    2 i 2005
    2 m 1975
    2 o 2005
    1 i 2005
    1 m 1975
    1 o 2005
    

    ... and then I see this:

    Calendar Widget ChangeLog
    0.01: First version
    0.02: Fix memory leak

    D'OH!

  • Ahh - is the calendar widget out of date?

  • Yes, i believe that was the problem.

  • Actually...
    "Calendar Widget" and "Calendar Week Widget" are two different widgets.
    The memory leak in the first one was fixed a while ago, but it looks like the other one still has the same bug.

  • Oh, wow, good point. Maybe i even mixed them up :)

  • Lol, the fixed one shows the wrong week 27 while the buggy one shows the correct week 30.

  • No, one shows the date, the other the week number. That's why there are two different widgets ;-)

  • That explains it. Sorry, i'm still in vacation mode.
    Can someone do a quick PR? Currently i have no chance to do it by myself. I'd really appreciate it.

  • Awesome, thanks!

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

RAM leak in my Watchface

Posted by Avatar for Hank @Hank

Actions