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 if (function.kind == FunctionKind.kTag) { |
| 187 tipOptimized = 'N/A'; |
| 188 } else { |
| 189 tipOptimized = |
| 190 Utils.formatPercent(node.profileFunction.inclusiveOptimizedTicks, |
| 191 node.profileFunction.inclusiveTicks); |
| 192 } |
| 193 } |
| 194 |
| 195 bool hasChildren() { |
| 196 return node.children.length > 0; |
| 197 } |
| 198 |
| 199 void _buildTooltip(DivElement memberList, Map<String, String> items) { |
| 200 items.forEach((k, v) { |
| 201 var item = new DivElement(); |
| 202 item.classes.add('memberItem'); |
| 203 var name = new DivElement(); |
| 204 name.classes.add('memberName'); |
| 205 name.classes.add('white'); |
| 206 name.text = k; |
| 207 var value = new DivElement(); |
| 208 value.classes.add('memberValue'); |
| 209 value.classes.add('white'); |
| 210 value.text = v; |
| 211 item.children.add(name); |
| 212 item.children.add(value); |
| 213 memberList.children.add(item); |
| 214 }); |
| 215 } |
| 216 |
| 217 void onShow() { |
| 218 super.onShow(); |
| 219 if (children.length == 0) { |
| 220 for (var childNode in node.children) { |
| 221 var row = new ProfileFunctionTrieNodeTreeRow(profile, |
| 222 root, |
| 223 childNode, tree, this); |
| 224 children.add(row); |
| 225 } |
| 226 } |
| 227 |
| 228 var methodCell = tableColumns[0]; |
| 229 // Enable expansion by clicking anywhere on the method column. |
| 230 methodCell.onClick.listen(onClick); |
| 231 |
| 232 // Grab the flex-row Div inside the methodCell. |
| 233 methodCell = methodCell.children[0]; |
| 234 |
| 235 // Insert the parent percentage |
| 236 var parentPercent = new DivElement(); |
| 237 parentPercent.text = tipParent; |
| 238 methodCell.children.add(parentPercent); |
| 239 |
| 240 var gap = new SpanElement(); |
| 241 gap.style.minWidth = '1em'; |
| 242 methodCell.children.add(gap); |
| 243 |
| 244 var functionAndCodeContainer = new DivElement(); |
| 245 methodCell.children.add(functionAndCodeContainer); |
| 246 |
| 247 var functionRef = new Element.tag('function-ref'); |
| 248 functionRef.ref = function; |
| 249 functionAndCodeContainer.children.add(functionRef); |
| 250 |
| 251 var codeRow = new DivElement(); |
| 252 codeRow.style.paddingTop = '1em'; |
| 253 functionAndCodeContainer.children.add(codeRow); |
| 254 if (!function.kind.isSynthetic()) { |
| 255 var totalInclusiveTicks = profileFunction.inclusiveTicks; |
| 256 for (var code in profileFunction.profileCodes) { |
| 257 var codeInclusiveTicks = code.inclusiveTicks; |
| 258 var inclusivePercentage = |
| 259 Utils.formatPercent(codeInclusiveTicks, totalInclusiveTicks); |
| 260 var percentageSpan = new SpanElement(); |
| 261 percentageSpan.text = '($inclusivePercentage) '; |
| 262 codeRow.children.add(percentageSpan); |
| 263 var codeRef = new Element.tag('code-ref'); |
| 264 codeRef.ref = code.code; |
| 265 codeRow.children.add(codeRef); |
| 266 } |
| 267 } |
| 268 |
| 269 var selfCell = tableColumns[1]; |
| 270 selfCell.style.position = 'relative'; |
| 271 selfCell.text = tipExclusive; |
| 272 |
| 273 var tooltipDiv = new DivElement(); |
| 274 tooltipDiv.classes.add('tooltip'); |
| 275 |
| 276 var memberListDiv = new DivElement(); |
| 277 memberListDiv.classes.add('memberList'); |
| 278 tooltipDiv.children.add(memberListDiv); |
| 279 _buildTooltip(memberListDiv, { |
| 280 'Kind' : tipKind, |
| 281 'Percent of Parent' : tipParent, |
| 282 'Sample Count' : tipTicks, |
| 283 'Approximate Execution Time': tipTime, |
| 284 'Optimized Execution Percentage': tipOptimized, |
| 285 }); |
| 286 selfCell.children.add(tooltipDiv); |
| 287 } |
| 288 } |
| 289 |
139 /// Displays a CpuProfile | 290 /// Displays a CpuProfile |
140 @CustomTag('cpu-profile') | 291 @CustomTag('cpu-profile') |
141 class CpuProfileElement extends ObservatoryElement { | 292 class CpuProfileElement extends ObservatoryElement { |
142 CpuProfileElement.created() : super.created(); | 293 static const MICROSECONDS_PER_SECOND = 1000000.0; |
| 294 |
143 @published Isolate isolate; | 295 @published Isolate isolate; |
144 | |
145 @observable ServiceMap profile; | |
146 @observable bool hideTagsChecked; | |
147 @observable String sampleCount = ''; | 296 @observable String sampleCount = ''; |
148 @observable String refreshTime = ''; | 297 @observable String refreshTime = ''; |
149 @observable String sampleRate = ''; | 298 @observable String sampleRate = ''; |
150 @observable String sampleDepth = ''; | 299 @observable String stackDepth = ''; |
151 @observable String displayCutoff = ''; | 300 @observable String displayCutoff = ''; |
152 @observable String timeSpan = ''; | 301 @observable String timeSpan = ''; |
153 @reflectable double displayThreshold = 0.0002; // 0.02%. | |
154 | 302 |
155 @observable String tagSelector = 'UserVM'; | 303 @observable String tagSelector = 'UserVM'; |
156 | 304 @observable String modeSelector = 'Function'; |
157 final _id = '#tableTree'; | 305 |
158 TableTree tree; | 306 final CpuProfile profile = new CpuProfile(); |
159 | 307 |
160 static const MICROSECONDS_PER_SECOND = 1000000.0; | 308 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 | 309 |
177 @override | 310 @override |
178 void attached() { | 311 void attached() { |
179 super.attached(); | 312 super.attached(); |
180 var tableBody = shadowRoot.querySelector('#tableTreeBody'); | 313 } |
181 assert(tableBody != null); | 314 |
182 tree = new TableTree(tableBody, 2); | 315 void isolateChanged(oldValue) { |
183 _update(); | 316 _getCpuProfile(); |
184 } | 317 } |
185 | 318 |
186 void tagSelectorChanged(oldValue) { | 319 void tagSelectorChanged(oldValue) { |
187 isolateChanged(null); | 320 _getCpuProfile(); |
| 321 } |
| 322 |
| 323 void modeSelectorChanged(oldValue) { |
| 324 _getCpuProfile(); |
188 } | 325 } |
189 | 326 |
190 void refresh(var done) { | 327 void refresh(var done) { |
191 isolate.invokeRpc('getCpuProfile', { 'tags': tagSelector }) | 328 _getCpuProfile().whenComplete(done); |
192 .then((ServiceObject obj) { | 329 } |
193 // Assert we got back the a profile. | 330 |
194 assert(obj.type == 'CpuProfile'); | 331 Future _getCpuProfile() { |
195 profile = obj; | 332 profile.clear(); |
196 _update(); | 333 if (isolate == null) { |
197 }).whenComplete(done); | 334 return new Future.value(null); |
198 } | 335 } |
199 | 336 return isolate.invokeRpc('getCpuProfile', { 'tags': tagSelector }) |
200 void _update() { | 337 .then((ServiceMap response) { |
201 if (profile == null) { | 338 profile.load(isolate, response); |
202 return; | 339 _updateView(); |
203 } | 340 }); |
204 var totalSamples = profile['samples']; | 341 } |
205 var now = new DateTime.now(); | 342 |
206 sampleCount = totalSamples.toString(); | 343 void _updateView() { |
207 refreshTime = now.toString(); | 344 sampleCount = profile.sampleCount.toString(); |
208 sampleDepth = profile['depth'].toString(); | 345 refreshTime = new DateTime.now().toString(); |
209 var period = profile['period']; | 346 stackDepth = profile.stackDepth.toString(); |
210 sampleRate = (MICROSECONDS_PER_SECOND / period).toStringAsFixed(0); | 347 sampleRate = profile.sampleRate.toStringAsFixed(0); |
211 timeSpan = formatTime(profile['timeSpan']); | 348 timeSpan = formatTime(profile.timeSpan); |
212 displayCutoff = '${(displayThreshold * 100.0).toString()}%'; | 349 displayCutoff = '${(profile.displayThreshold * 100.0).toString()}%'; |
213 profile.isolate.processProfile(profile); | 350 if (functionTree != null) { |
214 profile['threshold'] = displayThreshold; | 351 functionTree.clear(); |
215 _buildTree(); | 352 } |
216 } | 353 if (codeTree != null) { |
217 | 354 codeTree.clear(); |
218 void _buildStackTree() { | 355 } |
219 var root = profile.isolate.profileTrieRoot; | 356 if (modeSelector == 'Code') { |
| 357 _buildCodeTree(); |
| 358 } else { |
| 359 _buildFunctionTree(); |
| 360 } |
| 361 } |
| 362 |
| 363 TableTree codeTree; |
| 364 TableTree functionTree; |
| 365 |
| 366 void _buildFunctionTree() { |
| 367 if (functionTree == null) { |
| 368 var tableBody = shadowRoot.querySelector('#treeBody'); |
| 369 assert(tableBody != null); |
| 370 functionTree = new TableTree(tableBody, 2); |
| 371 } |
| 372 var root = profile.functionTrieRoot; |
220 if (root == null) { | 373 if (root == null) { |
221 return; | 374 return; |
222 } | 375 } |
223 try { | 376 try { |
224 tree.initialize( | 377 functionTree.initialize( |
225 new ProfileCodeTrieNodeTreeRow(profile, root, root, tree, null)); | 378 new ProfileFunctionTrieNodeTreeRow(profile, |
| 379 root, root, functionTree, null)); |
226 } catch (e, stackTrace) { | 380 } catch (e, stackTrace) { |
227 print(e); | 381 print(e); |
228 print(stackTrace); | 382 print(stackTrace); |
229 Logger.root.warning('_buildStackTree', e, stackTrace); | 383 Logger.root.warning('_buildFunctionTree', e, stackTrace); |
230 } | 384 } |
231 // Check if we only have one node at the root and expand it. | 385 // Check if we only have one node at the root and expand it. |
232 if (tree.rows.length == 1) { | 386 if (functionTree.rows.length == 1) { |
233 tree.toggle(tree.rows[0]); | 387 functionTree.toggle(functionTree.rows[0]); |
234 } | 388 } |
235 notifyPropertyChange(#tree, null, tree); | 389 } |
236 } | 390 |
237 | 391 void _buildCodeTree() { |
238 void _buildTree() { | 392 if (codeTree == null) { |
239 _buildStackTree(); | 393 var tableBody = shadowRoot.querySelector('#treeBody'); |
| 394 assert(tableBody != null); |
| 395 codeTree = new TableTree(tableBody, 2); |
| 396 } |
| 397 var root = profile.codeTrieRoot; |
| 398 if (root == null) { |
| 399 return; |
| 400 } |
| 401 try { |
| 402 codeTree.initialize( |
| 403 new ProfileCodeTrieNodeTreeRow(profile, root, root, codeTree, null)); |
| 404 } catch (e, stackTrace) { |
| 405 print(e); |
| 406 print(stackTrace); |
| 407 Logger.root.warning('_buildCodeTree', e, stackTrace); |
| 408 } |
| 409 // Check if we only have one node at the root and expand it. |
| 410 if (codeTree.rows.length == 1) { |
| 411 codeTree.toggle(codeTree.rows[0]); |
| 412 } |
240 } | 413 } |
241 } | 414 } |
OLD | NEW |