1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2008, 2009, 2010, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2008, 2009, 2010, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * 26 * @private 27 */ 28 AjxLeakDetector = function() { 29 this._controls = []; 30 this._closures = {}; // Map of id to { closure, args } 31 this._closureReport = []; // Report that is created during dispose event, not actually reported till later. 32 this._nextId = 1; 33 this._addHooks(); 34 }; 35 36 /** 37 * Executes a command. This is intended to be run by the client special search handler. 38 * 39 * @param {string} command "begin", "end", or "report" 40 * @return {hash} an object with 3 attributes: success, message, and details 41 */ 42 AjxLeakDetector.execute = 43 function(command) { 44 var result = { 45 success: false, 46 message: "", 47 details: "" 48 }; 49 if (command == "begin") { 50 result.success = AjxLeakDetector.begin(); 51 result.message = result.success ? "Leak detector started." : "Leak detector already started."; 52 } else if (command == "end") { 53 result.success = AjxLeakDetector.end(); 54 result.message = result.success ? "Leak detector stopped." : "Leak detector is not running."; 55 } else if (command == "report" || command == "dispose") { 56 if (command == "dispose") { 57 var shell = DwtShell.getShell(window); 58 shell.dispose(true); 59 document.title = "Shell has been disposed"; 60 } 61 var report = []; 62 result.success = AjxLeakDetector.report(report); 63 if (report.length) { 64 DBG.println("Leak detector report....."); 65 DBG.printRaw(report.join("")); 66 } 67 if (result.success) { 68 result.message = report.length ? "Problems found. See debug window for details." : "No problems found"; 69 } else { 70 result.message = "Leak detector is not running."; 71 } 72 } else { 73 result.success = false; 74 result.message = "Invalid argument, use (begin/end/report)"; 75 } 76 return result; 77 }; 78 79 AjxLeakDetector.begin = 80 function() { 81 if (!AjxLeakDetector._instance) { 82 AjxLeakDetector._instance = new AjxLeakDetector(); 83 return true; 84 } else { 85 return false; 86 } 87 }; 88 89 AjxLeakDetector.end = 90 function() { 91 if (AjxLeakDetector._instance) { 92 AjxLeakDetector._instance._removeHooks(); 93 AjxLeakDetector._instance = null; 94 return true; 95 } else { 96 return false; 97 } 98 }; 99 100 AjxLeakDetector.report = 101 function(report) { 102 if (AjxLeakDetector._instance) { 103 AjxLeakDetector._instance._createReport(report); 104 return true; 105 } else { 106 return false; 107 } 108 }; 109 110 AjxLeakDetector.prototype._addHooks = 111 function() { 112 var self = this; 113 114 // Hook into __initCtrl 115 var oldInit = DwtControl.prototype.__initCtrl; 116 DwtControl.prototype.__initCtrl = function() { 117 self._controls.push(this); 118 oldInit.call(this); 119 }; 120 121 // Hook into dispose. 122 var oldDispose = DwtControl.prototype.dispose; 123 DwtControl.prototype.dispose = function() { 124 var element = document.getElementById(this.getHTMLElId()); 125 oldDispose.call(this); 126 self._postDisposeCheck(this, element); 127 }; 128 129 // Hook into simple closure 130 var oldClosure = AjxCallback.simpleClosure; 131 AjxCallback.simpleClosure = function(func, obj) { 132 var result = oldClosure.apply(null, arguments); 133 result.__leakDetectorId = self._nextId++; 134 var args = []; 135 for (var i = 0, count = arguments.length; i < count; i++) { 136 args[i] = arguments[i]; 137 } 138 self._closures[result.__leakDetectorId] = { 139 closure: result, 140 args: args 141 }; 142 return result; 143 }; 144 145 // Create method for undoing this one. 146 this._removeHooks = function() { 147 DwtControl.prototype.__initCtrl = oldInit; 148 DwtControl.prototype.dispose = oldDispose; 149 AjxCallback.simpleClosure = oldClosure; 150 }; 151 }; 152 153 AjxLeakDetector.prototype._createReport = 154 function(report) { 155 for (var i = 0, count = this._controls.length; i < count; i++) { 156 var control = this._controls[i]; 157 158 // If the control believes it is still in play, make sure the html element is too. 159 if (!control._disposed) { 160 var element = document.getElementById(control.getHTMLElId()); 161 if (!element) { 162 this._log(report, "Detached html element", control); 163 } 164 } 165 // If the control has been disposed, make sure it doesn't directly reference any html elements. 166 else { 167 var elementNames = null; 168 for (var name in control) { 169 var value = control[name]; 170 if (value && value.tagName) { // I'm using tagName!=null to detect that it's an element. 171 elementNames = elementNames || []; 172 elementNames.push(name); 173 } 174 } 175 if (elementNames) { 176 this._log(report, "Elements referenced by control: " + elementNames.join(", "), control); 177 } 178 } 179 } 180 for (var i = 0, count = this._closureReport.length; i < count; i++) { 181 report.push(this._closureReport[i]); 182 } 183 if (report.length) { 184 return report.join(""); 185 } else { 186 return "Leak detector: no problems detected"; 187 } 188 }; 189 190 AjxLeakDetector.prototype._log = 191 function(report, message, control, element) { 192 report.push(message); 193 report.push("\n "); 194 var path = [control]; 195 while (control.parent) { 196 path.push(control.parent); 197 control = control.parent; 198 } 199 for (var i = path.length - 1; i >= 0; i--) { 200 report.push(path[i].toString()); 201 if (i > 0) { 202 report.push("->"); 203 } 204 } 205 this._logAttrs(report, path[0]); 206 report.push("\n------------------------\n"); 207 }; 208 209 AjxLeakDetector.prototype._logAttrs = 210 function(report, control) { 211 var attrMap = { 212 "DwtLabel" : ["__text", "__imageInfo"] 213 }; 214 215 var didIt = false; 216 for (var className in attrMap) { 217 if (Dwt.instanceOf(control, className)) { 218 if (!didIt) { 219 report.push("{\n"); 220 } 221 var attrs = attrMap[className]; 222 for (var i = 0, count = attrs.length; i < count; i++) { 223 report.push(" "); 224 report.push(attrs[i]); 225 report.push(": ") 226 report.push(control[attrs[i]]); 227 report.push("\n") 228 } 229 didIt = true; 230 } 231 } 232 if (didIt) { 233 report.push("}\n"); 234 } 235 }; 236 237 AjxLeakDetector.prototype._postDisposeCheck = 238 function(control, element) { 239 var report = []; 240 if (!element) { 241 this._log(this._closureReport, "Very bad: control's element not in DOM: " + report.join(""), control); 242 } else { 243 this._postDisposeElementCheck(report, element); 244 if (report.length) { 245 this._log(this._closureReport, "Suspicioius closure args in control: " + report.join(""), control); 246 } 247 } 248 }; 249 AjxLeakDetector.prototype._postDisposeElementCheck = 250 function(report, element) { 251 // Go thru all the element's properties looking for values that are simple closures. 252 var handlers = null; 253 for (var name in element) { 254 var argNames = null; 255 var value; 256 try { 257 value = element[name]; 258 } catch (e) { 259 // Certain properties aren't readable in ff, probably harmless, but report it... 260 DBG.println("AjxLeakDetector: error accessing property: " + name); 261 } 262 if (value && value.__leakDetectorId) { 263 var data = this._closures[value.__leakDetectorId]; 264 if (data) { 265 // Loop over the args that were passed to the closure... 266 var args = data.args; 267 for (var i = 0, count = args.length; i < count; i++) { 268 var arg = args[i]; 269 if (arg instanceof DwtControl) { 270 argNames = argNames || []; 271 argNames.push(arg.toString()); 272 } else if (arg.tagName) { // I'm using tagName!=null to detect that it's an element. 273 argNames = argNames || []; 274 argNames.push(arg.tagName); 275 } 276 } 277 } 278 } 279 if (argNames) { 280 handlers = handlers || []; 281 handlers.push(" "); 282 handlers.push(name); 283 handlers.push("("); 284 handlers.push(argNames.join(",")); 285 handlers.push(")\n"); 286 } 287 } 288 if (handlers) { 289 report.push("The element "); 290 report.push(element.tagName); 291 report.push("#"); 292 report.push(element.id || "noId"); 293 report.push("has the following handlers that may cause cirular references: \n"); 294 report.push(handlers.join("")); 295 } 296 var children = element.childNodes; 297 for (var i = 0, count = children.length; i < count; i++) { 298 this._postDisposeElementCheck(report, children[i]); 299 } 300 }; 301 302