PDA

View Full Version : memory leak - how to track down



DaveC426913
30 Sep 2010, 12:32 PM
I've got a memory leak. Not sure how I might go about chasing it down.

It's running on WinCE6.0.

I use Ajax to log in to a server, then start a WebWorker to poll the server for a single string of data. When it comes back, I populate a container to let me know it's still alive. I'm not interacting with the demo otherwise.

It polls every 3 seconds or so, but after about 500 tries, my system tells me it is very low on memory and I must shut down the browser.

I've tried this on a clean reboot, I've checked that the browser works fine without this code running.


Anyone have any suggestions about diagnosing it?

Note: I have VERY few system tools for analysis (assume none). This is a lean, mean sytem, absolute minimal required functionality. (I had to download and install Notepad fer Pete's Sake...)

If there's anything I can do within my code to start narrowing the problem, that would be ideal.

evant
30 Sep 2010, 3:21 PM
Post some code.

DaveC426913
1 Oct 2010, 5:58 AM
Thought I'd wait to hear some responses before dumping a giant block of code. I've been reading up on circular references from inner functions.

There's some add-to-list functionality in this demo that doesn't need to be examined since the memory leak occurs without any user interaction. The app now simply logs in and then polls the sever every few seconds, pulling down a line of data and populating a field.

Order of operations is:
1. App does nothing at all until Login is pressed.
2. login() makes an (Ext) Ajax call to get session ID and, if successful, calls uponLogin()
3. uponLogin() gets a shopping list then starts the pollingWorker() (pollingWorker was in a Web Worker. I've eliminated the Web Worker as a confounding factor by bringing that code into this page).
4. pollingWorker() makes a (raw)** Ajax call, pulls back a single row of data and hands it to pollingWorkerReport().
5. pollingWorkerReport() sees the header 'sections' and calls populateSections() to populate the sections fields (and display a perpetual counter to show that it's still alive.)
6. pollingWorker() repeats step 4 and 5 ad infinitum.

I ran this overnight and it died after 1158 iterations (about 1.2 hours, polling every 4 seconds).

** the pollingWorker uses a raw Ajax call instead of an Ext Ajax call because I originally coded it as a Web Worker, and WWs do not have access to Ext.js or to the DOM. It had to be raw Ajax.

I think I will isolate this further, ripping out unused functionality. Maybe I'll convert the pollingWorkers to an EXT-type ajax call for testing purposes.



Ext.setup({
onReady: function() {
var main = new Ext.Panel({
style: 'background-color: purple; padding:40px;color:white;font-size:9pt;',
fullscreen: true,
cls: 'main',
items:[
{
items:[
{
layout: 'hbox',
items:[
{ // login button
xtype: 'button',
id: 'btnLogin',
text: 'Login',
handler: loginButtonHandler,
},{ // logout button
xtype: 'button',
id: 'btnLogout',
text: 'Logout',
handler: logoutButtonHandler,
}]
}]
},{ // status wrapper
xtype: 'container',
id: 'statusWrapper',
height: 70,
style: 'font-size:8pt;margin-bottom: 10px',
scroll:'vertical',
items: [{ // status output
xtype: 'box',
id: 'statusBox',
}]
},{ // section output
xtype: 'container',
id: 'sections',
width: 250,
height: 85,
style: 'border:1px solid violet; padding: 4px;font-weight: bold;font-size:9pt;text-align:left;',
layout: 'vbox',
defaults: {
width:250,
},
items: [
{
xtype: 'container',
id: 'sectionAlive',
html: ' ',
},{
xtype: 'container',
id: 'sectionFunc',
html: 'Functional:'
},{
xtype: 'container',
id: 'sectionFlyer',
html: 'Flyer:'
},{
xtype: 'container',
id: 'sectionShop',
html: 'Shopping:'
},{
xtype: 'container',
id: 'sectionAd',
html: 'Ad:'
}]
}]
});


// *** VARIABLES ************
var baseUrl = 'http://server-s01.springboardnetworks.com:81/ws/';
var sessionId = null;
var url = baseUrl + "srnStoreDeviceSessionServices.php";
var did = "66:66:00:00:00:EE";
var loginTries = 0;
var logoutTries = 0;
var retryFreq = 5;
var giveUpAfter = 5;
var imAlive = 0;
// Some optional querystring parameters:
// &pollfreq=<integer> - defaults to 10(seconds)
// &verbose=true|false - defaults to false
var pollingFreq = parseInt(getQueryVariable('pollfreq')) || 10;//seconds
var verboseMsging = getQueryVariable('verbose') || false;

statusBox = Ext.get('statusBox');

var pollTimer;

// *** POLLING fns ************

function pollingWorker(e){
command = e[0];
switch (command){
case "start":
baseUrl = e[1];
did = e[2];
sessionId = e[3];
pollingFrequency = e[4];
// polling loop
this.pollTimer = setInterval( function(){
pollingWorkerReport(["status","Requesting sections..."]);
var url = baseUrl + "srnStoreDeviceAdServices.php";
Ext.Ajax.request({
url: url,
method: 'POST',
params :{fun:'srnGetSections',sid:sessionId},

// Ajax call succeeds
success: function(response, opts) {
pollingWorkerReport(["sections",response.responseText]);
},

// Ajax call fails
failure: function(response, opts) {
pollingWorkerReport(["error",response.responseText]);
},
});

}, pollingFrequency);
// polling loop
pollingWorkerReport(["status","Started"]);
break;
case "stop":
clearInterval(this.pollTimer);
pollingWorkerReport(["status","Stopped"]);
break;
}
};

function pollingWorkerReport(e){
// The message from the client:
pollHeader = e[0];
pollHeaderMsg = "POLLER " + pollHeader +": ";
pollMsg = e[1];
switch (pollHeader){
case "status"://pollworker is simply reporting its status
if(verboseMsging) { logmsg(pollHeaderMsg, pollMsg) }
break;
case "sections"://pollworker returns sections
if(verboseMsging) { logmsg(pollHeaderMsg, pollMsg); }
populateSections(string2json(pollMsg));
break;
default://pollworker returns something unrecognized
logmsg(pollHeaderMsg, pollMsg)
}
};
function populateSections(jsonObj){
imAlive++;
Ext.getCmp('sectionAlive').update(imAlive);
Ext.getCmp('sectionFunc').update("Functional: " + escapeHtml(jsonObj.fns));
Ext.getCmp('sectionFlyer').update("Flyer: " + escapeHtml(jsonObj.fls));
Ext.getCmp('sectionShop').update("Shopping: " + escapeHtml(jsonObj.sls));
Ext.getCmp('sectionAd').update("Ad: " + escapeHtml(jsonObj.aid));
};

// *** LOGIN/LOGOUT fns ************

function login(){
// provide did
// retrieve sid
logmsg("Requesting login...");
// Ajax call
Ext.Ajax.request({
url: url,
method: 'POST',
params :{fun:'srnDeviceLogin',did:did},

// Ajax call succeeds
success: function(response, opts) {
jsonObj = string2json(response.responseText);
// root of xml may come back as valid "items", or may come back as "error"
switch (jsonObj.RootName){
case "items":
sessionId = jsonObj.sid;
logmsg("Login successful. sid:" + sessionId);
pollingWorker(["start",baseUrl,did,sessionId,pollingFreq*1000]);
break;
default:
logmsg("ERROR retrieving sessionID. Login failed.");
logmsg(jsonObj);
retryLogin();
break;
}
},
// Ajax call fails
failure: function(response, opts) {
logmsg('Server-side failure with status code ' + response.status);
retryLogin();
},
});
};
function retryLogin(){
loginTries +=1;
if (loginTries < giveUpAfter){
logmsg("Try " + loginTries + " failed. Trying again in " + retryFreq + " seconds...")
setTimeout(function(){ login(); },retryFreq*1000);
}
else{
logmsg("Given up after " + loginTries + " tries.");
}
}

function logout(){
// called from button
// provide sid
pollingWorker(["stop"]);
logmsg("Requesting logout...")

// Ajax call
Ext.Ajax.request({
url: url,
method: 'POST',
params :{fun:'srnDeviceLogout',sid:sessionId},
// Ajax call succeeds
success: function(response, opts) {
jsonObj = string2json(response.responseText);
// root of xml may come back as valid "items", or may come back as "error"
switch (jsonObj.RootName){
case "items":
if (jsonObj.rcd==0){
logmsg("Logout successful.");
}
else{
logmsg("Logout failed with rcd ."+ jsonObj.rcd);
retryLogout();
}
break;
default:
logmsg("ERROR Logging out.");
retryLogout();
break;
}
},
// Ajax call fails
failure: function(response, opts) {
logmsg('Server-side failure with status code ' + response.status);
logmsg(jsonObj);
retryLogout();
},
});
};
function retryLogout(){
logoutTries +=1;
if (logoutTries < giveUpAfter){
logmsg("Try " + logoutTries + " failed. Trying again in " + retryFreq + " seconds...")
setTimeout(function(){ logout(); },retryFreq*1000);
}
else{
logmsg("Given up after " + logoutTries + " tries.");
}
};
function loginButtonHandler(){
loginTries = 0;
login();
};
function logoutButtonHandler(){
logoutTries = 0;
logout();
};


// *** GENERIC fns ************

function logmsg(msg,msgData){
if (msgData > ""){
console.log(msg + msgData);
statusBox.insertHtml("afterBegin",msg + json2string(msgData) + "<br/>");
}
else{
console.log(msg);
statusBox.insertHtml("afterBegin",msg + "<br/>");
}
};
function string2xml(string){
return XMLObjectifier.textToXML(string);
}

function xml2json(xmlObj){
return XMLObjectifier.xmlToJSON(xmlObj," ");
};
function string2json(string){
return xml2json(string2xml(string))
}

function json2string(jsonObj){
string = jsonObj.toString();
string = string.replace("<","&lt;")
string = string.replace(">","&gt;")
return string;
}
function getQueryVariable(variable) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if (pair[0] == variable) {
return pair[1];
}
}
}

function escapeHtml(string){
return string.replace("&","&amp;");
}
}
});



[EDIT: pared away some extraneous code.]
[EDIT: pared away all extraneous code. Down from 522 to 329 lines.]

Also, just realized you won't be able to run it without linking to this XML parser called saXMLUtils.js:



/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var XMLObjectifier = (function() {
var _clone = function(obj){
if(!!obj && typeof(obj)==="object"){
function F(){}
F.prototype = obj;
return new F();
}
};
//Is Numeric check
var isNumeric = function(s) {
var testStr = "";
if(!!s && typeof(s) === "string") { testStr = s; }
var pattern = /^((-)?([0-9]*)((\.{0,1})([0-9]+))?$)/;
return pattern.test(testStr);
};
var _self = {
xmlToJSON: function(xdoc) {
try {
if(!xdoc){ return null; }
var tmpObj = {};
tmpObj.typeOf = "JSXBObject";
var xroot = (xdoc.nodeType == 9)?xdoc.documentElement:xdoc;
tmpObj.RootName = xroot.nodeName || "";
if(xdoc.nodeType == 3 || xdoc.nodeType == 4) {
return xdoc.nodeValue;
}
//Trim function
function trim(s) {
return s.replace(/^\s+|\s+$/gm,'');
}
//Alters attribute and collection names to comply with JS
function formatName(name) {
var regEx = /-/g;
var tName = String(name).replace(regEx,"_");
return tName;
}
//Set Attributes of an object
function setAttributes(obj, node) {
if(node.attributes.length > 0) {
var a = node.attributes.length-1;
var attName;
obj._attributes = [];
do { //Order is irrelevant (speed-up)
attName = String(formatName(node.attributes[a].name));
obj._attributes.push(attName);
obj[attName] = trim(node.attributes[a].value);
} while(a--);
}
}

//Node Prototype
var _node = (function() {
var _self = {
activate: function() {
var nodes = [];
if(!!nodes) {
nodes.getNodesByAttribute = function(attr, obj) {
if(!!nodes && nodes.length > 0) {
var out = [];
var cNode;
var maxLen = nodes.length -1;
try {
do {
cNode = nodes[maxLen];
if(cNode[attr] === obj) {
out.push(cNode);
}
} while(maxLen--);
out.reverse();
return out;
} catch(e) {return null;}
return null;
}
};
nodes.getNodeByAttribute = function(attr, obj) {
if(!!nodes && nodes.length > 0) {
var cNode;
var maxLen = nodes.length -1;
try {
do {
cNode = nodes[maxLen];
if(cNode[attr] === obj) {
return cNode;
}
} while(maxLen--);
} catch(e) {return null;}
return null;
}
};
nodes.getNodesByValue = function(obj) {
if(!!nodes && nodes.length > 0) {
var out = [];
var cNode;
var maxLen = nodes.length -1;
try {
do {
cNode = nodes[maxLen];
if(!!cNode.Text && cNode.Text === obj) {
out.push(cNode);
}
} while(maxLen--);
return out;
} catch(e) {return null;}
return null;
}
};
nodes.contains = function(attr, obj) {
if(!!nodes && nodes.length > 0) {
var maxLen = nodes.length -1;
try {
do {
if(nodes[maxLen][attr] === obj) {
return true;
}
} while(maxLen--);
} catch(e) {return false;}
return false;
}
};
nodes.indexOf = function(attr, obj) {
var pos = -1;
if(!!nodes && nodes.length > 0) {
var maxLen = nodes.length -1;
try {
do {
if(nodes[maxLen][attr] === obj) {
pos = maxLen;
}
} while(maxLen--);
} catch(e) {return -1;}
return pos;
}
};
nodes.SortByAttribute = function(col, dir) {
if(!!nodes && nodes.length > 0) {
function getValue(pair, idx) {
var out = pair[idx];
out = (bam.validation.isNumeric(out))?parseFloat(out):out;
return out;
}
function sortFn(a, b) {
var tA, tB;
tA = getValue(a, col);
tB = getValue(b, col);
var res = (tA<tB)?-1:(tB<tA)?1:0;
if(!!dir) {
res = (dir.toUpperCase() === "DESC")?(0 - res):res;
}
return res;
}
nodes.sort(sortFn);
}
};
nodes.SortByValue = function(dir) {
if(!!nodes && nodes.length > 0) {
function getValue(pair) {
var out = pair.Text;
out = (bam.validation.isNumeric(out))?parseFloat(out):out;
return out;
}
function sortFn(a, b) {
var tA, tB;
tA = getValue(a);
tB = getValue(b);
var res = (tA<tB)?-1:(tB<tA)?1:0;
if(!!dir) {
res = (dir.toUpperCase() === "DESC")?(0 - res):res;
}
return res;
}
nodes.sort(sortFn);
}
};
nodes.SortByNode = function(node, dir) {
if(!!nodes && nodes.length > 0) {
function getValue(pair, node) {
var out = pair[node][0].Text;
out = (bam.validation.isNumeric(out))?parseFloat(out):out;
return out;
}
function sortFn(a, b) {
var tA, tB;
tA = getValue(a, node);
tB = getValue(b, node);
var res = (tA<tB)?-1:(tB<tA)?1:0;
if(!!dir) {
res = (dir.toUpperCase() === "DESC")?(0 - res):res;
}
return res;
}
nodes.sort(sortFn);
}
};
}
return nodes;
}
};
return _self;
})();
//Makes a new node of type _node;
var makeNode = function() {
var _fn = _clone(_node);
return _fn.activate();
}
//Set collections
function setHelpers(grpObj) {
//Selects a node withing array where attribute = value
grpObj.getNodeByAttribute = function(attr, obj) {
if(this.length > 0) {
var cNode;
var maxLen = this.length -1;
try {
do {
cNode = this[maxLen];
if(cNode[attr] == obj) {
return cNode;
}
} while(maxLen--);
} catch(e) {return false;}
return false;
}
};

grpObj.contains = function(attr, obj) {
if(this.length > 0) {
var maxLen = this.length -1;
try {
do {
if(this[maxLen][attr] == obj) {
return true;
}
} while(maxLen--);
} catch(e) {return false;}
return false;
}
};

grpObj.indexOf = function(attr, obj) {
var pos = -1;
if(this.length > 0) {
var maxLen = this.length -1;
try {
do {
if(this[maxLen][attr] == obj) {
pos = maxLen;
}
} while(maxLen--);
} catch(e) {return -1;}
return pos;
}
};

grpObj.SortByAttribute = function(col, dir) {
if(this.length) {
function getValue(pair, idx) {
var out = pair[idx];
out = (isNumeric(out))?parseFloat(out):out;
return out;
}
function sortFn(a, b) {
var res = 0;
var tA, tB;
tA = getValue(a, col);
tB = getValue(b, col);
if(tA < tB) { res = -1; } else if(tB < tA) { res = 1; }
if(dir) {
res = (dir.toUpperCase() == "DESC")?(0 - res):res;
}
return res;
}
this.sort(sortFn);
}
};

grpObj.SortByValue = function(dir) {
if(this.length) {
function getValue(pair) {
var out = pair.Text;
out = (isNumeric(out))?parseFloat(out):out;
return out;
}
function sortFn(a, b) {
var res = 0;
var tA, tB;
tA = getValue(a);
tB = getValue(b);
if(tA < tB) { res = -1; } else if(tB < tA) { res = 1; }
if(dir) {
res = (dir.toUpperCase() == "DESC")?(0 - res):res;
}
return res;
}
this.sort(sortFn);
}
};

grpObj.SortByNode = function(node, dir) {
if(this.length) {
function getValue(pair, node) {
var out = pair[node][0].Text;
out = (isNumeric(out))?parseFloat(out):out;
return out;
}
function sortFn(a, b) {
var res = 0;
var tA, tB;
tA = getValue(a, node);
tB = getValue(b, node);
if(tA < tB) { res = -1; } else if(tB < tA) { res = 1; }
if(dir) {
res = (dir.toUpperCase() == "DESC")?(0 - res):res;
}
return res;
}
this.sort(sortFn);
}
};
}
//Recursive JSON Assembler
//Set Object Nodes
function setObjects(obj, node) {
var elemName; //Element name
var cnode; //Current Node
var tObj; //New subnode
var cName = "";
if(!node) { return null; }
//Set node attributes if any
if(node.attributes.length > 0){setAttributes(obj, node);}
obj.Text = "";
if(node.hasChildNodes()) {
var nodeCount = node.childNodes.length - 1;
var n = 0;
do { //Order is irrelevant (speed-up)
cnode = node.childNodes[n];
switch(cnode.nodeType) {
case 1: //Node
//Process child nodes
obj._children = [];
//SOAP XML FIX to remove namespaces (i.e. soapenv:)
elemName = (cnode.localName)?cnode.localName:cnode.baseName;
elemName = formatName(elemName);
if(cName != elemName) { obj._children.push(elemName); }
//Create sub elemns array
if(!obj[elemName]) {
obj[elemName] = []; //Create Collection
}
tObj = {};
obj[elemName].push(tObj);
if(cnode.attributes.length > 0) {
setAttributes(tObj, cnode);
}
//Set Helper functions (contains, indexOf, sort, etc);
if(!obj[elemName].contains) {
setHelpers(obj[elemName]);
}
cName = elemName;
if(cnode.hasChildNodes()) {
setObjects(tObj, cnode); //Recursive Call
}
break;
case 3: //Text Value
obj.Text += trim(cnode.nodeValue);
break;
case 4: //CDATA
obj.Text += (cnode.text)?trim(cnode.text):trim(cnode.nodeValue);
break;
}
} while(n++ < nodeCount);
}
}
//RUN
setObjects(tmpObj, xroot);
//Clean-up memmory
xdoc = null;
xroot = null;
return tmpObj;
} catch(e) {
return null;
}
},
//Converts Text to XML DOM
textToXML: function(strXML) {
var xmlDoc = null;
try {
xmlDoc = (document.all)?new ActiveXObject("Microsoft.XMLDOM"):new DOMParser();
xmlDoc.async = false;
} catch(e) {throw new Error("XML Parser could not be instantiated");}
var out;
try {
if(document.all) {
out = (xmlDoc.loadXML(strXML))?xmlDoc:false;
} else {
out = xmlDoc.parseFromString(strXML, "text/xml");
}
} catch(e) { throw new Error("Error parsing XML string"); }
return out;
}
};
return _self;
})();

DaveC426913
1 Oct 2010, 9:51 AM
OK, I've just run my barebones code again under ideal conditions. It died at about 2000 iterations. (Can't tell exactly how many cuz the %&#$ error dialogue is blocking my output display.)

Are there any diagnostic tests I might insert in my code to chase down what's leaking?

DaveC426913
4 Oct 2010, 9:32 AM
I've now ripped out everything - including all the Ajax - and left only the setInterval. It's still leaking. It leaks a few k every second.

The culprit is updating the container. If I comment out the a.update(imAlive), then no leak.

Is there a better way of displaying dynamic content onscreen than updating a container's html?


Ext.setup({
onReady: function() {
var main = new Ext.Panel({
style: 'background-color: purple; padding:40px;color:white;font-size:9pt;',
fullscreen: true,
cls: 'main',
items:[
{
items:[
{
layout: 'hbox',
items:[
{ // login button
xtype: 'button',
id: 'btnStart',
text: 'Start',
handler: startButtonHandler,
},{ // logout button
xtype: 'button',
id: 'btnStop',
text: 'Stop',
handler: stopButtonHandler,
}]
}]
},{ // section output
xtype: 'container',
id: 'sectionAlive',
html: '&nbsp;',
width: 250,
height: 85,
style: 'border:1px solid violet; padding: 4px;font-weight: bold;font-size:9pt;text-align:left;',
}]
});

var imAlive = 0;
a = Ext.getCmp('sectionAlive')
var pollTimer;


function startButtonHandler(){
pollTimer = setInterval( function(){
imAlive++;
a.update(imAlive);
}, 100);
};
function stopButtonHandler(){
clearInterval(pollTimer);
};

}
});