| OLD | NEW |
| 1 /* | 1 /* |
| 2 * Copyright (C) 2008 Apple Inc. All Rights Reserved. | 2 * Copyright (C) 2008 Apple Inc. All Rights Reserved. |
| 3 * | 3 * |
| 4 * Redistribution and use in source and binary forms, with or without | 4 * Redistribution and use in source and binary forms, with or without |
| 5 * modification, are permitted provided that the following conditions | 5 * modification, are permitted provided that the following conditions |
| 6 * are met: | 6 * are met: |
| 7 * 1. Redistributions of source code must retain the above copyright | 7 * 1. Redistributions of source code must retain the above copyright |
| 8 * notice, this list of conditions and the following disclaimer. | 8 * notice, this list of conditions and the following disclaimer. |
| 9 * 2. Redistributions in binary form must reproduce the above copyright | 9 * 2. Redistributions in binary form must reproduce the above copyright |
| 10 * notice, this list of conditions and the following disclaimer in the | 10 * notice, this list of conditions and the following disclaimer in the |
| 11 * documentation and/or other materials provided with the distribution. | 11 * documentation and/or other materials provided with the distribution. |
| 12 * | 12 * |
| 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY | 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | 15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR | 16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, | 17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, | 18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR | 19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY | 20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | 21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | 22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 24 */ | 24 */ |
| 25 | 25 |
| 26 | 26 |
| 27 /** | 27 /** |
| 28 * @constructor | 28 * @constructor |
| 29 * @param {!ProfilerAgent.CPUProfile} profile | |
| 30 */ | |
| 31 WebInspector.CPUProfileDataModel = function(profile) | |
| 32 { | |
| 33 this.profileHead = profile.head; | |
| 34 this.samples = profile.samples; | |
| 35 this._calculateTimes(profile); | |
| 36 this._assignParentsInProfile(); | |
| 37 if (this.samples) | |
| 38 this._buildIdToNodeMap(); | |
| 39 } | |
| 40 | |
| 41 WebInspector.CPUProfileDataModel.prototype = { | |
| 42 /** | |
| 43 * @param {!ProfilerAgent.CPUProfile} profile | |
| 44 */ | |
| 45 _calculateTimes: function(profile) | |
| 46 { | |
| 47 function totalHitCount(node) { | |
| 48 var result = node.hitCount; | |
| 49 for (var i = 0; i < node.children.length; i++) | |
| 50 result += totalHitCount(node.children[i]); | |
| 51 return result; | |
| 52 } | |
| 53 profile.totalHitCount = totalHitCount(profile.head); | |
| 54 | |
| 55 var durationMs = 1000 * (profile.endTime - profile.startTime); | |
| 56 var samplingInterval = durationMs / profile.totalHitCount; | |
| 57 this.samplingIntervalMs = samplingInterval; | |
| 58 | |
| 59 function calculateTimesForNode(node) { | |
| 60 node.selfTime = node.hitCount * samplingInterval; | |
| 61 var totalHitCount = node.hitCount; | |
| 62 for (var i = 0; i < node.children.length; i++) | |
| 63 totalHitCount += calculateTimesForNode(node.children[i]); | |
| 64 node.totalTime = totalHitCount * samplingInterval; | |
| 65 return totalHitCount; | |
| 66 } | |
| 67 calculateTimesForNode(profile.head); | |
| 68 }, | |
| 69 | |
| 70 _assignParentsInProfile: function() | |
| 71 { | |
| 72 var head = this.profileHead; | |
| 73 head.parent = null; | |
| 74 head.head = null; | |
| 75 var nodesToTraverse = [ head ]; | |
| 76 while (nodesToTraverse.length) { | |
| 77 var parent = nodesToTraverse.pop(); | |
| 78 var children = parent.children; | |
| 79 var length = children.length; | |
| 80 for (var i = 0; i < length; ++i) { | |
| 81 var child = children[i]; | |
| 82 child.head = head; | |
| 83 child.parent = parent; | |
| 84 if (child.children.length) | |
| 85 nodesToTraverse.push(child); | |
| 86 } | |
| 87 } | |
| 88 }, | |
| 89 | |
| 90 _buildIdToNodeMap: function() | |
| 91 { | |
| 92 /** @type {!Object.<number, !ProfilerAgent.CPUProfileNode>} */ | |
| 93 this._idToNode = {}; | |
| 94 var idToNode = this._idToNode; | |
| 95 var stack = [this.profileHead]; | |
| 96 while (stack.length) { | |
| 97 var node = stack.pop(); | |
| 98 idToNode[node.id] = node; | |
| 99 for (var i = 0; i < node.children.length; i++) | |
| 100 stack.push(node.children[i]); | |
| 101 } | |
| 102 | |
| 103 var topLevelNodes = this.profileHead.children; | |
| 104 for (var i = 0; i < topLevelNodes.length; i++) { | |
| 105 var node = topLevelNodes[i]; | |
| 106 if (node.functionName === "(garbage collector)") { | |
| 107 this._gcNode = node; | |
| 108 break; | |
| 109 } | |
| 110 } | |
| 111 } | |
| 112 } | |
| 113 | |
| 114 | |
| 115 /** | |
| 116 * @constructor | |
| 117 * @extends {WebInspector.VBox} | 29 * @extends {WebInspector.VBox} |
| 118 * @param {!WebInspector.CPUProfileHeader} profileHeader | 30 * @param {!WebInspector.CPUProfileHeader} profileHeader |
| 119 */ | 31 */ |
| 120 WebInspector.CPUProfileView = function(profileHeader) | 32 WebInspector.CPUProfileView = function(profileHeader) |
| 121 { | 33 { |
| 122 WebInspector.VBox.call(this); | 34 WebInspector.VBox.call(this); |
| 123 this.element.classList.add("cpu-profile-view"); | 35 this.element.classList.add("cpu-profile-view"); |
| 124 | 36 |
| 125 this._viewType = WebInspector.settings.createSetting("cpuProfilerView", WebI
nspector.CPUProfileView._TypeHeavy); | 37 this._viewType = WebInspector.settings.createSetting("cpuProfilerView", WebI
nspector.CPUProfileView._TypeHeavy); |
| 126 | 38 |
| (...skipping 862 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 989 { | 901 { |
| 990 if (!WebInspector.CPUProfileView._colorGenerator) { | 902 if (!WebInspector.CPUProfileView._colorGenerator) { |
| 991 var colorGenerator = new WebInspector.CPUProfileFlameChart.ColorGenerato
r(); | 903 var colorGenerator = new WebInspector.CPUProfileFlameChart.ColorGenerato
r(); |
| 992 colorGenerator.colorForID("(idle)::0", 50); | 904 colorGenerator.colorForID("(idle)::0", 50); |
| 993 colorGenerator.colorForID("(program)::0", 50); | 905 colorGenerator.colorForID("(program)::0", 50); |
| 994 colorGenerator.colorForID("(garbage collector)::0", 50); | 906 colorGenerator.colorForID("(garbage collector)::0", 50); |
| 995 WebInspector.CPUProfileView._colorGenerator = colorGenerator; | 907 WebInspector.CPUProfileView._colorGenerator = colorGenerator; |
| 996 } | 908 } |
| 997 return WebInspector.CPUProfileView._colorGenerator; | 909 return WebInspector.CPUProfileView._colorGenerator; |
| 998 } | 910 } |
| 999 | |
| 1000 /** | |
| 1001 * @constructor | |
| 1002 * @implements {WebInspector.FlameChartDataProvider} | |
| 1003 * @param {!WebInspector.CPUProfileDataModel} cpuProfile | |
| 1004 * @param {!WebInspector.Target} target | |
| 1005 */ | |
| 1006 WebInspector.CPUFlameChartDataProvider = function(cpuProfile, target) | |
| 1007 { | |
| 1008 WebInspector.FlameChartDataProvider.call(this); | |
| 1009 this._cpuProfile = cpuProfile; | |
| 1010 this._target = target; | |
| 1011 this._colorGenerator = WebInspector.CPUProfileView.colorGenerator(); | |
| 1012 } | |
| 1013 | |
| 1014 WebInspector.CPUFlameChartDataProvider.prototype = { | |
| 1015 /** | |
| 1016 * @return {number} | |
| 1017 */ | |
| 1018 barHeight: function() | |
| 1019 { | |
| 1020 return 15; | |
| 1021 }, | |
| 1022 | |
| 1023 /** | |
| 1024 * @return {number} | |
| 1025 */ | |
| 1026 textBaseline: function() | |
| 1027 { | |
| 1028 return 4; | |
| 1029 }, | |
| 1030 | |
| 1031 /** | |
| 1032 * @return {number} | |
| 1033 */ | |
| 1034 textPadding: function() | |
| 1035 { | |
| 1036 return 2; | |
| 1037 }, | |
| 1038 | |
| 1039 /** | |
| 1040 * @param {number} startTime | |
| 1041 * @param {number} endTime | |
| 1042 * @return {?Array.<number>} | |
| 1043 */ | |
| 1044 dividerOffsets: function(startTime, endTime) | |
| 1045 { | |
| 1046 return null; | |
| 1047 }, | |
| 1048 | |
| 1049 /** | |
| 1050 * @return {number} | |
| 1051 */ | |
| 1052 zeroTime: function() | |
| 1053 { | |
| 1054 return 0; | |
| 1055 }, | |
| 1056 | |
| 1057 /** | |
| 1058 * @return {number} | |
| 1059 */ | |
| 1060 totalTime: function() | |
| 1061 { | |
| 1062 return this._cpuProfile.profileHead.totalTime; | |
| 1063 }, | |
| 1064 | |
| 1065 /** | |
| 1066 * @return {number} | |
| 1067 */ | |
| 1068 maxStackDepth: function() | |
| 1069 { | |
| 1070 return this._maxStackDepth; | |
| 1071 }, | |
| 1072 | |
| 1073 /** | |
| 1074 * @return {?WebInspector.FlameChart.TimelineData} | |
| 1075 */ | |
| 1076 timelineData: function() | |
| 1077 { | |
| 1078 return this._timelineData || this._calculateTimelineData(); | |
| 1079 }, | |
| 1080 | |
| 1081 /** | |
| 1082 * @return {?WebInspector.FlameChart.TimelineData} | |
| 1083 */ | |
| 1084 _calculateTimelineData: function() | |
| 1085 { | |
| 1086 if (!this._cpuProfile.profileHead) | |
| 1087 return null; | |
| 1088 | |
| 1089 var samples = this._cpuProfile.samples; | |
| 1090 var idToNode = this._cpuProfile._idToNode; | |
| 1091 var gcNode = this._cpuProfile._gcNode; | |
| 1092 var samplesCount = samples.length; | |
| 1093 var samplingInterval = this._cpuProfile.samplingIntervalMs; | |
| 1094 | |
| 1095 var index = 0; | |
| 1096 | |
| 1097 var openIntervals = []; | |
| 1098 var stackTrace = []; | |
| 1099 var maxDepth = 5; // minimum stack depth for the case when we see no act
ivity. | |
| 1100 var depth = 0; | |
| 1101 | |
| 1102 /** | |
| 1103 * @constructor | |
| 1104 * @param {number} depth | |
| 1105 * @param {number} duration | |
| 1106 * @param {number} startTime | |
| 1107 * @param {!Object} node | |
| 1108 */ | |
| 1109 function ChartEntry(depth, duration, startTime, node) | |
| 1110 { | |
| 1111 this.depth = depth; | |
| 1112 this.duration = duration; | |
| 1113 this.startTime = startTime; | |
| 1114 this.node = node; | |
| 1115 this.selfTime = 0; | |
| 1116 } | |
| 1117 var entries = /** @type {!Array.<!ChartEntry>} */ ([]); | |
| 1118 | |
| 1119 for (var sampleIndex = 0; sampleIndex < samplesCount; sampleIndex++) { | |
| 1120 var node = idToNode[samples[sampleIndex]]; | |
| 1121 stackTrace.length = 0; | |
| 1122 while (node) { | |
| 1123 stackTrace.push(node); | |
| 1124 node = node.parent; | |
| 1125 } | |
| 1126 stackTrace.pop(); // Remove (root) node | |
| 1127 | |
| 1128 maxDepth = Math.max(maxDepth, depth); | |
| 1129 depth = 0; | |
| 1130 node = stackTrace.pop(); | |
| 1131 var intervalIndex; | |
| 1132 | |
| 1133 // GC samples have no stack, so we just put GC node on top of the la
st recoreded sample. | |
| 1134 if (node === gcNode) { | |
| 1135 while (depth < openIntervals.length) { | |
| 1136 intervalIndex = openIntervals[depth].index; | |
| 1137 entries[intervalIndex].duration += samplingInterval; | |
| 1138 ++depth; | |
| 1139 } | |
| 1140 // If previous stack is also GC then just continue. | |
| 1141 if (openIntervals.length > 0 && openIntervals.peekLast().node ==
= node) { | |
| 1142 entries[intervalIndex].selfTime += samplingInterval; | |
| 1143 continue; | |
| 1144 } | |
| 1145 } | |
| 1146 | |
| 1147 while (node && depth < openIntervals.length && node === openInterval
s[depth].node) { | |
| 1148 intervalIndex = openIntervals[depth].index; | |
| 1149 entries[intervalIndex].duration += samplingInterval; | |
| 1150 node = stackTrace.pop(); | |
| 1151 ++depth; | |
| 1152 } | |
| 1153 if (depth < openIntervals.length) | |
| 1154 openIntervals.length = depth; | |
| 1155 if (!node) { | |
| 1156 entries[intervalIndex].selfTime += samplingInterval; | |
| 1157 continue; | |
| 1158 } | |
| 1159 | |
| 1160 var colorGenerator = this._colorGenerator; | |
| 1161 var color = ""; | |
| 1162 while (node) { | |
| 1163 entries.push(new ChartEntry(depth, samplingInterval, sampleIndex
* samplingInterval, node)); | |
| 1164 openIntervals.push({node: node, index: index}); | |
| 1165 ++index; | |
| 1166 | |
| 1167 node = stackTrace.pop(); | |
| 1168 ++depth; | |
| 1169 } | |
| 1170 entries[entries.length - 1].selfTime += samplingInterval; | |
| 1171 } | |
| 1172 | |
| 1173 /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */ | |
| 1174 var entryNodes = new Array(entries.length); | |
| 1175 var entryLevels = new Uint8Array(entries.length); | |
| 1176 var entryTotalTimes = new Float32Array(entries.length); | |
| 1177 var entrySelfTimes = new Float32Array(entries.length); | |
| 1178 var entryOffsets = new Float32Array(entries.length); | |
| 1179 | |
| 1180 for (var i = 0; i < entries.length; ++i) { | |
| 1181 var entry = entries[i]; | |
| 1182 entryNodes[i] = entry.node; | |
| 1183 entryLevels[i] = entry.depth; | |
| 1184 entryTotalTimes[i] = entry.duration; | |
| 1185 entryOffsets[i] = entry.startTime; | |
| 1186 entrySelfTimes[i] = entry.selfTime; | |
| 1187 } | |
| 1188 | |
| 1189 this._maxStackDepth = Math.max(maxDepth, depth); | |
| 1190 | |
| 1191 this._timelineData = { | |
| 1192 entryLevels: entryLevels, | |
| 1193 entryTotalTimes: entryTotalTimes, | |
| 1194 entryOffsets: entryOffsets, | |
| 1195 }; | |
| 1196 | |
| 1197 /** @type {!Array.<!ProfilerAgent.CPUProfileNode>} */ | |
| 1198 this._entryNodes = entryNodes; | |
| 1199 this._entrySelfTimes = entrySelfTimes; | |
| 1200 | |
| 1201 return /** @type {!WebInspector.FlameChart.TimelineData} */ (this._timel
ineData); | |
| 1202 }, | |
| 1203 | |
| 1204 /** | |
| 1205 * @param {number} ms | |
| 1206 * @return {string} | |
| 1207 */ | |
| 1208 _millisecondsToString: function(ms) | |
| 1209 { | |
| 1210 if (ms === 0) | |
| 1211 return "0"; | |
| 1212 if (ms < 1000) | |
| 1213 return WebInspector.UIString("%.1f\u2009ms", ms); | |
| 1214 return Number.secondsToString(ms / 1000, true); | |
| 1215 }, | |
| 1216 | |
| 1217 /** | |
| 1218 * @param {number} entryIndex | |
| 1219 * @return {?Array.<!{title: string, text: string}>} | |
| 1220 */ | |
| 1221 prepareHighlightedEntryInfo: function(entryIndex) | |
| 1222 { | |
| 1223 var timelineData = this._timelineData; | |
| 1224 var node = this._entryNodes[entryIndex]; | |
| 1225 if (!node) | |
| 1226 return null; | |
| 1227 | |
| 1228 var entryInfo = []; | |
| 1229 function pushEntryInfoRow(title, text) | |
| 1230 { | |
| 1231 var row = {}; | |
| 1232 row.title = title; | |
| 1233 row.text = text; | |
| 1234 entryInfo.push(row); | |
| 1235 } | |
| 1236 | |
| 1237 pushEntryInfoRow(WebInspector.UIString("Name"), node.functionName); | |
| 1238 var selfTime = this._millisecondsToString(this._entrySelfTimes[entryInde
x]); | |
| 1239 var totalTime = this._millisecondsToString(timelineData.entryTotalTimes[
entryIndex]); | |
| 1240 pushEntryInfoRow(WebInspector.UIString("Self time"), selfTime); | |
| 1241 pushEntryInfoRow(WebInspector.UIString("Total time"), totalTime); | |
| 1242 var target = this._target; | |
| 1243 var text = WebInspector.Linkifier.liveLocationText(target, node.scriptId
, node.lineNumber, node.columnNumber); | |
| 1244 pushEntryInfoRow(WebInspector.UIString("URL"), text); | |
| 1245 pushEntryInfoRow(WebInspector.UIString("Aggregated self time"), Number.s
econdsToString(node.selfTime / 1000, true)); | |
| 1246 pushEntryInfoRow(WebInspector.UIString("Aggregated total time"), Number.
secondsToString(node.totalTime / 1000, true)); | |
| 1247 if (node.deoptReason && node.deoptReason !== "no reason") | |
| 1248 pushEntryInfoRow(WebInspector.UIString("Not optimized"), node.deoptR
eason); | |
| 1249 | |
| 1250 return entryInfo; | |
| 1251 }, | |
| 1252 | |
| 1253 /** | |
| 1254 * @param {number} entryIndex | |
| 1255 * @return {boolean} | |
| 1256 */ | |
| 1257 canJumpToEntry: function(entryIndex) | |
| 1258 { | |
| 1259 return this._entryNodes[entryIndex].scriptId !== "0"; | |
| 1260 }, | |
| 1261 | |
| 1262 /** | |
| 1263 * @param {number} entryIndex | |
| 1264 * @return {?string} | |
| 1265 */ | |
| 1266 entryTitle: function(entryIndex) | |
| 1267 { | |
| 1268 var node = this._entryNodes[entryIndex]; | |
| 1269 return node.functionName; | |
| 1270 }, | |
| 1271 | |
| 1272 /** | |
| 1273 * @param {number} entryIndex | |
| 1274 * @return {?string} | |
| 1275 */ | |
| 1276 entryFont: function(entryIndex) | |
| 1277 { | |
| 1278 if (!this._font) { | |
| 1279 this._font = (this.barHeight() - 4) + "px " + WebInspector.fontFamil
y(); | |
| 1280 this._boldFont = "bold " + this._font; | |
| 1281 } | |
| 1282 var node = this._entryNodes[entryIndex]; | |
| 1283 var reason = node.deoptReason; | |
| 1284 return (reason && reason !== "no reason") ? this._boldFont : this._font; | |
| 1285 }, | |
| 1286 | |
| 1287 /** | |
| 1288 * @param {number} entryIndex | |
| 1289 * @return {!string} | |
| 1290 */ | |
| 1291 entryColor: function(entryIndex) | |
| 1292 { | |
| 1293 var node = this._entryNodes[entryIndex]; | |
| 1294 return this._colorGenerator.colorForID(node.functionName + ":" + node.ur
l + ":" + node.lineNumber); | |
| 1295 }, | |
| 1296 | |
| 1297 /** | |
| 1298 * @param {number} entryIndex | |
| 1299 * @param {!CanvasRenderingContext2D} context | |
| 1300 * @param {?string} text | |
| 1301 * @param {number} barX | |
| 1302 * @param {number} barY | |
| 1303 * @param {number} barWidth | |
| 1304 * @param {number} barHeight | |
| 1305 * @param {function(number):number} offsetToPosition | |
| 1306 * @return {boolean} | |
| 1307 */ | |
| 1308 decorateEntry: function(entryIndex, context, text, barX, barY, barWidth, bar
Height, offsetToPosition) | |
| 1309 { | |
| 1310 return false; | |
| 1311 }, | |
| 1312 | |
| 1313 /** | |
| 1314 * @param {number} entryIndex | |
| 1315 * @return {boolean} | |
| 1316 */ | |
| 1317 forceDecoration: function(entryIndex) | |
| 1318 { | |
| 1319 return false; | |
| 1320 }, | |
| 1321 | |
| 1322 /** | |
| 1323 * @param {number} entryIndex | |
| 1324 * @return {!{startTimeOffset: number, endTimeOffset: number}} | |
| 1325 */ | |
| 1326 highlightTimeRange: function(entryIndex) | |
| 1327 { | |
| 1328 var startTimeOffset = this._timelineData.entryOffsets[entryIndex]; | |
| 1329 return { | |
| 1330 startTimeOffset: startTimeOffset, | |
| 1331 endTimeOffset: startTimeOffset + this._timelineData.entryTotalTimes[
entryIndex] | |
| 1332 }; | |
| 1333 }, | |
| 1334 | |
| 1335 /** | |
| 1336 * @return {number} | |
| 1337 */ | |
| 1338 paddingLeft: function() | |
| 1339 { | |
| 1340 return 15; | |
| 1341 }, | |
| 1342 | |
| 1343 /** | |
| 1344 * @param {number} entryIndex | |
| 1345 * @return {!string} | |
| 1346 */ | |
| 1347 textColor: function(entryIndex) | |
| 1348 { | |
| 1349 return "#333"; | |
| 1350 } | |
| 1351 } | |
| OLD | NEW |