OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library cpu_profile_element; | 5 library cpu_profile_element; |
6 | 6 |
| 7 import 'dart:async'; |
7 import 'dart:html'; | 8 import 'dart:html'; |
8 import 'observatory_element.dart'; | 9 import 'observatory_element.dart'; |
9 import 'package:logging/logging.dart'; | 10 import 'package:logging/logging.dart'; |
10 import 'package:observatory/service.dart'; | 11 import 'package:observatory/service.dart'; |
11 import 'package:observatory/app.dart'; | 12 import 'package:observatory/app.dart'; |
| 13 import 'package:observatory/cpu_profile.dart'; |
12 import 'package:observatory/elements.dart'; | 14 import 'package:observatory/elements.dart'; |
13 import 'package:polymer/polymer.dart'; | 15 import 'package:polymer/polymer.dart'; |
14 | 16 |
15 class ProfileCodeTrieNodeTreeRow extends TableTreeRow { | 17 class ProfileCodeTrieNodeTreeRow extends TableTreeRow { |
16 final ServiceMap profile; | 18 final CpuProfile profile; |
17 @reflectable final CodeTrieNode root; | 19 @reflectable final CodeTrieNode root; |
18 @reflectable final CodeTrieNode node; | 20 @reflectable final CodeTrieNode node; |
19 @reflectable Code get code => node.code; | 21 @reflectable Code get code => node.profileCode.code; |
20 | 22 |
21 @reflectable String tipKind = ''; | 23 @reflectable String tipKind = ''; |
22 @reflectable String tipParent = ''; | 24 @reflectable String tipParent = ''; |
23 @reflectable String tipExclusive = ''; | 25 @reflectable String tipExclusive = ''; |
24 @reflectable String tipTicks = ''; | 26 @reflectable String tipTicks = ''; |
25 @reflectable String tipTime = ''; | 27 @reflectable String tipTime = ''; |
26 | 28 |
27 ProfileCodeTrieNodeTreeRow(this.profile, this.root, this.node, | 29 ProfileCodeTrieNodeTreeRow(this.profile, this.root, this.node, |
28 TableTree tree, | 30 TableTree tree, |
29 ProfileCodeTrieNodeTreeRow parent) | 31 ProfileCodeTrieNodeTreeRow parent) |
30 : super(tree, parent) { | 32 : super(tree, parent) { |
31 assert(root != null); | 33 assert(root != null); |
32 assert(node != null); | 34 assert(node != null); |
33 tipTicks = '${node.count}'; | 35 tipTicks = '${node.count}'; |
34 var period = profile['period']; | 36 var seconds = profile.approximateSecondsForCount(node.count); |
35 var MICROSECONDS_PER_SECOND = 1000000.0; | |
36 var seconds = (period * node.count) / MICROSECONDS_PER_SECOND; // seconds | |
37 tipTime = Utils.formatTimePrecise(seconds); | 37 tipTime = Utils.formatTimePrecise(seconds); |
38 if (code.kind == CodeKind.Tag) { | 38 if (code.kind == CodeKind.Tag) { |
39 tipKind = 'Tag (category)'; | 39 tipKind = 'Tag (category)'; |
40 if (parent == null) { | 40 if (parent == null) { |
41 tipParent = Utils.formatPercent(node.count, root.count); | 41 tipParent = Utils.formatPercent(node.count, root.count); |
42 } else { | 42 } else { |
43 tipParent = Utils.formatPercent(node.count, parent.node.count); | 43 tipParent = Utils.formatPercent(node.count, parent.node.count); |
44 } | 44 } |
45 tipExclusive = Utils.formatPercent(node.count, root.count); | 45 tipExclusive = Utils.formatPercent(node.count, root.count); |
46 } else { | 46 } else { |
47 if ((code.kind == CodeKind.Collected) || | 47 if ((code.kind == CodeKind.Collected) || |
48 (code.kind == CodeKind.Reused)) { | 48 (code.kind == CodeKind.Reused)) { |
49 tipKind = 'Garbage Collected Code'; | 49 tipKind = 'Garbage Collected Code'; |
50 } else { | 50 } else { |
51 tipKind = '${code.kind} (Function)'; | 51 tipKind = '${code.kind} (Function)'; |
52 } | 52 } |
53 if (parent == null) { | 53 if (parent == null) { |
54 tipParent = Utils.formatPercent(node.count, root.count); | 54 tipParent = Utils.formatPercent(node.count, root.count); |
55 } else { | 55 } else { |
56 tipParent = Utils.formatPercent(node.count, parent.node.count); | 56 tipParent = Utils.formatPercent(node.count, parent.node.count); |
57 } | 57 } |
58 tipExclusive = Utils.formatPercent(node.code.exclusiveTicks, root.count); | 58 tipExclusive = |
| 59 Utils.formatPercent(node.profileCode.exclusiveTicks, root.count); |
59 } | 60 } |
60 } | 61 } |
61 | 62 |
62 bool shouldDisplayChild(CodeTrieNode childNode, double threshold) { | 63 bool shouldDisplayChild(CodeTrieNode childNode, double threshold) { |
63 return ((childNode.count / node.count) > threshold) || | 64 return ((childNode.count / node.count) > threshold) || |
64 ((childNode.code.exclusiveTicks / root.count) > threshold); | 65 ((childNode.profileCode.exclusiveTicks / root.count) > threshold); |
65 } | 66 } |
66 | 67 |
67 void _buildTooltip(DivElement memberList, Map<String, String> items) { | 68 void _buildTooltip(DivElement memberList, Map<String, String> items) { |
68 items.forEach((k, v) { | 69 items.forEach((k, v) { |
69 var item = new DivElement(); | 70 var item = new DivElement(); |
70 item.classes.add('memberItem'); | 71 item.classes.add('memberItem'); |
71 var name = new DivElement(); | 72 var name = new DivElement(); |
72 name.classes.add('memberName'); | 73 name.classes.add('memberName'); |
73 name.classes.add('white'); | 74 name.classes.add('white'); |
74 name.text = k; | 75 name.text = k; |
75 var value = new DivElement(); | 76 var value = new DivElement(); |
76 value.classes.add('memberValue'); | 77 value.classes.add('memberValue'); |
77 value.classes.add('white'); | 78 value.classes.add('white'); |
78 value.text = v; | 79 value.text = v; |
79 item.children.add(name); | 80 item.children.add(name); |
80 item.children.add(value); | 81 item.children.add(value); |
81 memberList.children.add(item); | 82 memberList.children.add(item); |
82 }); | 83 }); |
83 } | 84 } |
84 | 85 |
85 void onShow() { | 86 void onShow() { |
86 super.onShow(); | 87 super.onShow(); |
87 if (children.length == 0) { | 88 if (children.length == 0) { |
88 var threshold = profile['threshold']; | 89 var threshold = profile.displayThreshold; |
89 for (var childNode in node.children) { | 90 for (var childNode in node.children) { |
90 if (!shouldDisplayChild(childNode, threshold)) { | 91 if (!shouldDisplayChild(childNode, threshold)) { |
91 continue; | 92 continue; |
92 } | 93 } |
93 var row = | 94 var row = |
94 new ProfileCodeTrieNodeTreeRow(profile, root, childNode, tree, this)
; | 95 new ProfileCodeTrieNodeTreeRow(profile, root, childNode, tree, this); |
95 children.add(row); | 96 children.add(row); |
96 } | 97 } |
97 } | 98 } |
98 var row = tr; | |
99 | 99 |
100 var methodCell = tableColumns[0]; | 100 var methodCell = tableColumns[0]; |
101 // Enable expansion by clicking anywhere on the method column. | 101 // Enable expansion by clicking anywhere on the method column. |
102 methodCell.onClick.listen(onClick); | 102 methodCell.onClick.listen(onClick); |
103 | 103 |
| 104 // Grab the flex-row Div inside the methodCell. |
| 105 methodCell = methodCell.children[0]; |
| 106 |
104 // Insert the parent percentage | 107 // Insert the parent percentage |
105 var parentPercent = new DivElement(); | 108 var parentPercent = new DivElement(); |
106 parentPercent.style.position = 'relative'; | |
107 parentPercent.style.display = 'inline'; | |
108 parentPercent.text = tipParent; | 109 parentPercent.text = tipParent; |
109 methodCell.children.add(parentPercent); | 110 methodCell.children.add(parentPercent); |
110 | 111 |
| 112 var gap = new SpanElement(); |
| 113 gap.style.minWidth = '1em'; |
| 114 methodCell.children.add(gap); |
| 115 |
111 var codeRef = new Element.tag('code-ref'); | 116 var codeRef = new Element.tag('code-ref'); |
112 codeRef.ref = code; | 117 codeRef.ref = code; |
113 methodCell.children.add(codeRef); | 118 methodCell.children.add(codeRef); |
114 | 119 |
115 var selfCell = tableColumns[1]; | 120 var selfCell = tableColumns[1]; |
116 selfCell.style.position = 'relative'; | 121 selfCell.style.position = 'relative'; |
117 selfCell.text = tipExclusive; | 122 selfCell.text = tipExclusive; |
118 | 123 |
119 var tooltipDiv = new DivElement(); | 124 var tooltipDiv = new DivElement(); |
120 tooltipDiv.classes.add('tooltip'); | 125 tooltipDiv.classes.add('tooltip'); |
121 | 126 |
122 var memberListDiv = new DivElement(); | 127 var memberListDiv = new DivElement(); |
123 memberListDiv.classes.add('memberList'); | 128 memberListDiv.classes.add('memberList'); |
124 tooltipDiv.children.add(memberListDiv); | 129 tooltipDiv.children.add(memberListDiv); |
125 _buildTooltip(memberListDiv, { | 130 _buildTooltip(memberListDiv, { |
126 'Kind' : tipKind, | 131 'Kind' : tipKind, |
127 'Percent of Parent' : tipParent, | 132 'Percent of Parent' : tipParent, |
128 'Sample Count' : tipTicks, | 133 'Sample Count' : tipTicks, |
129 'Approximate Execution Time': tipTime, | 134 'Approximate Execution Time': tipTime, |
130 }); | 135 }); |
131 selfCell.children.add(tooltipDiv); | 136 selfCell.children.add(tooltipDiv); |
132 } | 137 } |
133 | 138 |
134 bool hasChildren() { | 139 bool hasChildren() { |
135 return node.children.length > 0; | 140 return node.children.length > 0; |
136 } | 141 } |
137 } | 142 } |
138 | 143 |
| 144 class ProfileFunctionTrieNodeTreeRow extends TableTreeRow { |
| 145 final CpuProfile profile; |
| 146 @reflectable final FunctionTrieNode root; |
| 147 @reflectable final FunctionTrieNode node; |
| 148 ProfileFunction get profileFunction => node.profileFunction; |
| 149 @reflectable ServiceFunction get function => node.profileFunction.function; |
| 150 @reflectable String tipKind = ''; |
| 151 @reflectable String tipParent = ''; |
| 152 @reflectable String tipExclusive = ''; |
| 153 @reflectable String tipTime = ''; |
| 154 @reflectable String tipTicks = ''; |
| 155 |
| 156 String tipOptimized = ''; |
| 157 |
| 158 ProfileFunctionTrieNodeTreeRow(this.profile, this.root, this.node, |
| 159 TableTree tree, |
| 160 ProfileFunctionTrieNodeTreeRow parent) |
| 161 : super(tree, parent) { |
| 162 assert(root != null); |
| 163 assert(node != null); |
| 164 tipTicks = '${node.count}'; |
| 165 var seconds = profile.approximateSecondsForCount(node.count); |
| 166 tipTime = Utils.formatTimePrecise(seconds); |
| 167 if (parent == null) { |
| 168 tipParent = Utils.formatPercent(node.count, root.count); |
| 169 } else { |
| 170 tipParent = Utils.formatPercent(node.count, parent.node.count); |
| 171 } |
| 172 if (function.kind == FunctionKind.kTag) { |
| 173 tipExclusive = Utils.formatPercent(node.count, root.count); |
| 174 } else { |
| 175 tipExclusive = |
| 176 Utils.formatPercent(node.profileFunction.exclusiveTicks, root.count); |
| 177 } |
| 178 |
| 179 if (function.kind == FunctionKind.kTag) { |
| 180 tipKind = 'Tag (category)'; |
| 181 } else if (function.kind == FunctionKind.kCollected) { |
| 182 tipKind = 'Garbage Collected Code'; |
| 183 } else { |
| 184 tipKind = '${function.kind} (Function)'; |
| 185 } |
| 186 } |
| 187 |
| 188 bool hasChildren() { |
| 189 return node.children.length > 0; |
| 190 } |
| 191 |
| 192 void _buildTooltip(DivElement memberList, Map<String, String> items) { |
| 193 items.forEach((k, v) { |
| 194 var item = new DivElement(); |
| 195 item.classes.add('memberItem'); |
| 196 var name = new DivElement(); |
| 197 name.classes.add('memberName'); |
| 198 name.classes.add('white'); |
| 199 name.text = k; |
| 200 var value = new DivElement(); |
| 201 value.classes.add('memberValue'); |
| 202 value.classes.add('white'); |
| 203 value.text = v; |
| 204 item.children.add(name); |
| 205 item.children.add(value); |
| 206 memberList.children.add(item); |
| 207 }); |
| 208 } |
| 209 |
| 210 void onShow() { |
| 211 super.onShow(); |
| 212 if (children.length == 0) { |
| 213 for (var childNode in node.children) { |
| 214 var row = new ProfileFunctionTrieNodeTreeRow(profile, |
| 215 root, |
| 216 childNode, tree, this); |
| 217 children.add(row); |
| 218 } |
| 219 } |
| 220 |
| 221 var methodCell = tableColumns[0]; |
| 222 // Enable expansion by clicking anywhere on the method column. |
| 223 methodCell.onClick.listen(onClick); |
| 224 |
| 225 // Grab the flex-row Div inside the methodCell. |
| 226 methodCell = methodCell.children[0]; |
| 227 |
| 228 // Insert the parent percentage |
| 229 var parentPercent = new DivElement(); |
| 230 parentPercent.text = tipParent; |
| 231 methodCell.children.add(parentPercent); |
| 232 |
| 233 var gap = new SpanElement(); |
| 234 gap.style.minWidth = '1em'; |
| 235 methodCell.children.add(gap); |
| 236 |
| 237 var functionAndCodeContainer = new DivElement(); |
| 238 methodCell.children.add(functionAndCodeContainer); |
| 239 |
| 240 var functionRef = new Element.tag('function-ref'); |
| 241 functionRef.ref = function; |
| 242 functionAndCodeContainer.children.add(functionRef); |
| 243 |
| 244 var codeRow = new DivElement(); |
| 245 codeRow.style.paddingTop = '1em'; |
| 246 functionAndCodeContainer.children.add(codeRow); |
| 247 if (!function.kind.isSynthetic()) { |
| 248 |
| 249 var totalTicks = node.totalCodesTicks; |
| 250 var numCodes = node.codes.length; |
| 251 |
| 252 for (var i = 0; i < numCodes; i++) { |
| 253 var codeRowSpan = new DivElement(); |
| 254 codeRow.children.add(codeRowSpan); |
| 255 var nodeCode = node.codes[i]; |
| 256 var ticks = nodeCode.ticks; |
| 257 var percentage = Utils.formatPercent(ticks, totalTicks); |
| 258 var percentageSpan = new SpanElement(); |
| 259 percentageSpan.text = '($percentage) '; |
| 260 codeRowSpan.children.add(percentageSpan); |
| 261 var codeRef = new Element.tag('code-ref'); |
| 262 codeRef.ref = nodeCode.code.code; |
| 263 codeRowSpan.children.add(codeRef); |
| 264 } |
| 265 |
| 266 } |
| 267 |
| 268 var selfCell = tableColumns[1]; |
| 269 selfCell.style.position = 'relative'; |
| 270 selfCell.text = tipExclusive; |
| 271 |
| 272 var tooltipDiv = new DivElement(); |
| 273 tooltipDiv.classes.add('tooltip'); |
| 274 |
| 275 var memberListDiv = new DivElement(); |
| 276 memberListDiv.classes.add('memberList'); |
| 277 tooltipDiv.children.add(memberListDiv); |
| 278 _buildTooltip(memberListDiv, { |
| 279 'Kind' : tipKind, |
| 280 'Percent of Parent' : tipParent, |
| 281 'Sample Count' : tipTicks, |
| 282 'Approximate Execution Time': tipTime, |
| 283 }); |
| 284 selfCell.children.add(tooltipDiv); |
| 285 } |
| 286 } |
| 287 |
139 /// Displays a CpuProfile | 288 /// Displays a CpuProfile |
140 @CustomTag('cpu-profile') | 289 @CustomTag('cpu-profile') |
141 class CpuProfileElement extends ObservatoryElement { | 290 class CpuProfileElement extends ObservatoryElement { |
142 CpuProfileElement.created() : super.created(); | 291 static const MICROSECONDS_PER_SECOND = 1000000.0; |
| 292 |
143 @published Isolate isolate; | 293 @published Isolate isolate; |
144 | |
145 @observable ServiceMap profile; | |
146 @observable bool hideTagsChecked; | |
147 @observable String sampleCount = ''; | 294 @observable String sampleCount = ''; |
148 @observable String refreshTime = ''; | 295 @observable String refreshTime = ''; |
149 @observable String sampleRate = ''; | 296 @observable String sampleRate = ''; |
150 @observable String sampleDepth = ''; | 297 @observable String stackDepth = ''; |
151 @observable String displayCutoff = ''; | 298 @observable String displayCutoff = ''; |
152 @observable String timeSpan = ''; | 299 @observable String timeSpan = ''; |
153 @reflectable double displayThreshold = 0.0002; // 0.02%. | |
154 | 300 |
155 @observable String tagSelector = 'UserVM'; | 301 @observable String tagSelector = 'UserVM'; |
156 | 302 @observable String modeSelector = 'Function'; |
157 final _id = '#tableTree'; | 303 |
158 TableTree tree; | 304 final CpuProfile profile = new CpuProfile(); |
159 | 305 |
160 static const MICROSECONDS_PER_SECOND = 1000000.0; | 306 CpuProfileElement.created() : super.created(); |
161 | |
162 void isolateChanged(oldValue) { | |
163 if (isolate == null) { | |
164 profile = null; | |
165 return; | |
166 } | |
167 isolate.invokeRpc('getCpuProfile', { 'tags': tagSelector }) | |
168 .then((ServiceObject obj) { | |
169 print(obj); | |
170 // Assert we got back the a profile. | |
171 assert(obj.type == 'CpuProfile'); | |
172 profile = obj; | |
173 _update(); | |
174 }); | |
175 } | |
176 | 307 |
177 @override | 308 @override |
178 void attached() { | 309 void attached() { |
179 super.attached(); | 310 super.attached(); |
180 var tableBody = shadowRoot.querySelector('#tableTreeBody'); | 311 } |
181 assert(tableBody != null); | 312 |
182 tree = new TableTree(tableBody, 2); | 313 void isolateChanged(oldValue) { |
183 _update(); | 314 _getCpuProfile(); |
184 } | 315 } |
185 | 316 |
186 void tagSelectorChanged(oldValue) { | 317 void tagSelectorChanged(oldValue) { |
187 isolateChanged(null); | 318 _getCpuProfile(); |
| 319 } |
| 320 |
| 321 void modeSelectorChanged(oldValue) { |
| 322 _updateView(); |
| 323 } |
| 324 |
| 325 void clear(var done) { |
| 326 _clearCpuProfile().whenComplete(done); |
| 327 } |
| 328 |
| 329 Future _clearCpuProfile() { |
| 330 profile.clear(); |
| 331 if (isolate == null) { |
| 332 return new Future.value(null); |
| 333 } |
| 334 return isolate.invokeRpc('clearCpuProfile', { }) |
| 335 .then((ServiceMap response) { |
| 336 _updateView(); |
| 337 }); |
188 } | 338 } |
189 | 339 |
190 void refresh(var done) { | 340 void refresh(var done) { |
191 isolate.invokeRpc('getCpuProfile', { 'tags': tagSelector }) | 341 _getCpuProfile().whenComplete(done); |
192 .then((ServiceObject obj) { | 342 } |
193 // Assert we got back the a profile. | 343 |
194 assert(obj.type == 'CpuProfile'); | 344 Future _getCpuProfile() { |
195 profile = obj; | 345 profile.clear(); |
196 _update(); | 346 if (isolate == null) { |
197 }).whenComplete(done); | 347 return new Future.value(null); |
198 } | 348 } |
199 | 349 return isolate.invokeRpc('getCpuProfile', { 'tags': tagSelector }) |
200 void _update() { | 350 .then((ServiceMap response) { |
201 if (profile == null) { | 351 profile.load(isolate, response); |
202 return; | 352 _updateView(); |
203 } | 353 }); |
204 var totalSamples = profile['samples']; | 354 } |
205 var now = new DateTime.now(); | 355 |
206 sampleCount = totalSamples.toString(); | 356 void _updateView() { |
207 refreshTime = now.toString(); | 357 sampleCount = profile.sampleCount.toString(); |
208 sampleDepth = profile['depth'].toString(); | 358 refreshTime = new DateTime.now().toString(); |
209 var period = profile['period']; | 359 stackDepth = profile.stackDepth.toString(); |
210 sampleRate = (MICROSECONDS_PER_SECOND / period).toStringAsFixed(0); | 360 sampleRate = profile.sampleRate.toStringAsFixed(0); |
211 timeSpan = formatTime(profile['timeSpan']); | 361 timeSpan = formatTime(profile.timeSpan); |
212 displayCutoff = '${(displayThreshold * 100.0).toString()}%'; | 362 displayCutoff = '${(profile.displayThreshold * 100.0).toString()}%'; |
213 profile.isolate.processProfile(profile); | 363 if (functionTree != null) { |
214 profile['threshold'] = displayThreshold; | 364 functionTree.clear(); |
215 _buildTree(); | 365 } |
216 } | 366 if (codeTree != null) { |
217 | 367 codeTree.clear(); |
218 void _buildStackTree() { | 368 } |
219 var root = profile.isolate.profileTrieRoot; | 369 if (modeSelector == 'Code') { |
| 370 _buildCodeTree(); |
| 371 } else { |
| 372 _buildFunctionTree(); |
| 373 } |
| 374 } |
| 375 |
| 376 TableTree codeTree; |
| 377 TableTree functionTree; |
| 378 |
| 379 void _buildFunctionTree() { |
| 380 if (functionTree == null) { |
| 381 var tableBody = shadowRoot.querySelector('#treeBody'); |
| 382 assert(tableBody != null); |
| 383 functionTree = new TableTree(tableBody, 2); |
| 384 } |
| 385 var root = profile.functionTrieRoot; |
220 if (root == null) { | 386 if (root == null) { |
221 return; | 387 return; |
222 } | 388 } |
223 try { | 389 try { |
224 tree.initialize( | 390 functionTree.initialize( |
225 new ProfileCodeTrieNodeTreeRow(profile, root, root, tree, null)); | 391 new ProfileFunctionTrieNodeTreeRow(profile, |
| 392 root, root, functionTree, null)); |
226 } catch (e, stackTrace) { | 393 } catch (e, stackTrace) { |
227 print(e); | 394 print(e); |
228 print(stackTrace); | 395 print(stackTrace); |
229 Logger.root.warning('_buildStackTree', e, stackTrace); | 396 Logger.root.warning('_buildFunctionTree', e, stackTrace); |
230 } | 397 } |
231 // Check if we only have one node at the root and expand it. | 398 // Check if we only have one node at the root and expand it. |
232 if (tree.rows.length == 1) { | 399 if (functionTree.rows.length == 1) { |
233 tree.toggle(tree.rows[0]); | 400 functionTree.toggle(functionTree.rows[0]); |
234 } | 401 } |
235 notifyPropertyChange(#tree, null, tree); | 402 } |
236 } | 403 |
237 | 404 void _buildCodeTree() { |
238 void _buildTree() { | 405 if (codeTree == null) { |
239 _buildStackTree(); | 406 var tableBody = shadowRoot.querySelector('#treeBody'); |
| 407 assert(tableBody != null); |
| 408 codeTree = new TableTree(tableBody, 2); |
| 409 } |
| 410 var root = profile.codeTrieRoot; |
| 411 if (root == null) { |
| 412 return; |
| 413 } |
| 414 try { |
| 415 codeTree.initialize( |
| 416 new ProfileCodeTrieNodeTreeRow(profile, root, root, codeTree, null)); |
| 417 } catch (e, stackTrace) { |
| 418 print(e); |
| 419 print(stackTrace); |
| 420 Logger.root.warning('_buildCodeTree', e, stackTrace); |
| 421 } |
| 422 // Check if we only have one node at the root and expand it. |
| 423 if (codeTree.rows.length == 1) { |
| 424 codeTree.toggle(codeTree.rows[0]); |
| 425 } |
240 } | 426 } |
241 } | 427 } |
OLD | NEW |