OLD | NEW |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 /** |
| 5 * @unrestricted |
| 6 */ |
| 7 WebInspector.NetworkTimelineColumn = class extends WebInspector.VBox { |
| 8 /** |
| 9 * @param {number} rowHeight |
| 10 * @param {!WebInspector.NetworkTimeCalculator} calculator |
| 11 */ |
| 12 constructor(rowHeight, calculator) { |
| 13 // TODO(allada) Make this a shadowDOM when the NetworkTimelineColumn gets mo
ved into NetworkLogViewColumns. |
| 14 super(false); |
| 15 this.registerRequiredCSS('network/networkTimelineColumn.css'); |
4 | 16 |
5 /** | 17 this._canvas = this.contentElement.createChild('canvas'); |
6 * @constructor | |
7 * @extends {WebInspector.VBox} | |
8 * @param {number} rowHeight | |
9 * @param {!WebInspector.NetworkTimeCalculator} calculator | |
10 */ | |
11 WebInspector.NetworkTimelineColumn = function(rowHeight, calculator) | |
12 { | |
13 // TODO(allada) Make this a shadowDOM when the NetworkTimelineColumn gets mo
ved into NetworkLogViewColumns. | |
14 WebInspector.VBox.call(this, false); | |
15 this.registerRequiredCSS("network/networkTimelineColumn.css"); | |
16 | |
17 this._canvas = this.contentElement.createChild("canvas"); | |
18 this._canvas.tabIndex = 1; | 18 this._canvas.tabIndex = 1; |
19 this.setDefaultFocusedElement(this._canvas); | 19 this.setDefaultFocusedElement(this._canvas); |
20 this._canvasPosition = this._canvas.getBoundingClientRect(); | 20 this._canvasPosition = this._canvas.getBoundingClientRect(); |
21 | 21 |
22 /** @const */ | 22 /** @const */ |
23 this._leftPadding = 5; | 23 this._leftPadding = 5; |
24 /** @const */ | 24 /** @const */ |
25 this._fontSize = 10; | 25 this._fontSize = 10; |
26 | 26 |
27 this._rightPadding = 0; | 27 this._rightPadding = 0; |
(...skipping 14 matching lines...) Expand all Loading... |
42 /** @type {?WebInspector.NetworkRequest.InitiatorGraph} */ | 42 /** @type {?WebInspector.NetworkRequest.InitiatorGraph} */ |
43 this._initiatorGraph = null; | 43 this._initiatorGraph = null; |
44 | 44 |
45 /** @type {?WebInspector.NetworkRequest} */ | 45 /** @type {?WebInspector.NetworkRequest} */ |
46 this._navigationRequest = null; | 46 this._navigationRequest = null; |
47 | 47 |
48 /** @type {!Map<string, !Array<number>>} */ | 48 /** @type {!Map<string, !Array<number>>} */ |
49 this._eventDividers = new Map(); | 49 this._eventDividers = new Map(); |
50 | 50 |
51 var colorUsage = WebInspector.ThemeSupport.ColorUsage; | 51 var colorUsage = WebInspector.ThemeSupport.ColorUsage; |
52 this._rowNavigationRequestColor = WebInspector.themeSupport.patchColor("#def
", colorUsage.Background); | 52 this._rowNavigationRequestColor = WebInspector.themeSupport.patchColor('#def
', colorUsage.Background); |
53 this._rowStripeColor = WebInspector.themeSupport.patchColor("#f5f5f5", color
Usage.Background); | 53 this._rowStripeColor = WebInspector.themeSupport.patchColor('#f5f5f5', color
Usage.Background); |
54 this._rowHoverColor = WebInspector.themeSupport.patchColor("#ebf2fc", /** @t
ype {!WebInspector.ThemeSupport.ColorUsage} */ (colorUsage.Background | colorUsa
ge.Selection)); | 54 this._rowHoverColor = WebInspector.themeSupport.patchColor( |
55 this._parentInitiatorColor = WebInspector.themeSupport.patchColor("hsla(120,
68%, 54%, 0.2)", colorUsage.Background); | 55 '#ebf2fc', /** @type {!WebInspector.ThemeSupport.ColorUsage} */ (colorUs
age.Background | colorUsage.Selection)); |
56 this._initiatedColor = WebInspector.themeSupport.patchColor("hsla(0, 68%, 54
%, 0.2)", colorUsage.Background); | 56 this._parentInitiatorColor = |
| 57 WebInspector.themeSupport.patchColor('hsla(120, 68%, 54%, 0.2)', colorUs
age.Background); |
| 58 this._initiatedColor = WebInspector.themeSupport.patchColor('hsla(0, 68%, 54
%, 0.2)', colorUsage.Background); |
57 | 59 |
58 /** @type {!Map<!WebInspector.ResourceType, string>} */ | 60 /** @type {!Map<!WebInspector.ResourceType, string>} */ |
59 this._borderColorsForResourceTypeCache = new Map(); | 61 this._borderColorsForResourceTypeCache = new Map(); |
60 /** @type {!Map<string, !CanvasGradient>} */ | 62 /** @type {!Map<string, !CanvasGradient>} */ |
61 this._colorsForResourceTypeCache = new Map(); | 63 this._colorsForResourceTypeCache = new Map(); |
| 64 } |
| 65 |
| 66 /** |
| 67 * @override |
| 68 */ |
| 69 willHide() { |
| 70 this._popoverHelper.hidePopover(); |
| 71 } |
| 72 |
| 73 /** |
| 74 * @override |
| 75 */ |
| 76 wasShown() { |
| 77 this.update(); |
| 78 } |
| 79 |
| 80 /** |
| 81 * @param {!Element} element |
| 82 * @param {!Event} event |
| 83 * @return {!AnchorBox|undefined} |
| 84 */ |
| 85 _getPopoverAnchor(element, event) { |
| 86 if (!this._hoveredRequest) |
| 87 return; |
| 88 |
| 89 var range = WebInspector.RequestTimingView.calculateRequestTimeRanges(this._
hoveredRequest, 0) |
| 90 .find(data => data.name === 'total'); |
| 91 var start = this._timeToPosition(range.start); |
| 92 var end = this._timeToPosition(range.end); |
| 93 |
| 94 if (event.clientX < this._canvasPosition.left + start || event.clientX > thi
s._canvasPosition.left + end) |
| 95 return; |
| 96 |
| 97 var rowIndex = this._requestData.findIndex(request => this._hoveredRequest =
== request); |
| 98 var barHeight = this._getBarHeight(range.name); |
| 99 var y = this._headerHeight + (this._rowHeight * rowIndex - this._scrollTop)
+ ((this._rowHeight - barHeight) / 2); |
| 100 |
| 101 if (event.clientY < this._canvasPosition.top + y || event.clientY > this._ca
nvasPosition.top + y + barHeight) |
| 102 return; |
| 103 |
| 104 var anchorBox = this.element.boxInWindow(); |
| 105 anchorBox.x += start; |
| 106 anchorBox.y += y; |
| 107 anchorBox.width = end - start; |
| 108 anchorBox.height = barHeight; |
| 109 return anchorBox; |
| 110 } |
| 111 |
| 112 /** |
| 113 * @param {!Element|!AnchorBox} anchor |
| 114 * @param {!WebInspector.Popover} popover |
| 115 */ |
| 116 _showPopover(anchor, popover) { |
| 117 if (!this._hoveredRequest) |
| 118 return; |
| 119 var content = |
| 120 WebInspector.RequestTimingView.createTimingTable(this._hoveredRequest, t
his._calculator.minimumBoundary()); |
| 121 popover.showForAnchor(content, anchor); |
| 122 } |
| 123 |
| 124 /** |
| 125 * @param {?WebInspector.NetworkRequest} request |
| 126 * @param {boolean} highlightInitiatorChain |
| 127 */ |
| 128 setHoveredRequest(request, highlightInitiatorChain) { |
| 129 this._hoveredRequest = request; |
| 130 this._initiatorGraph = (highlightInitiatorChain && request) ? request.initia
torGraph() : null; |
| 131 this.update(); |
| 132 } |
| 133 |
| 134 /** |
| 135 * @param {number} height |
| 136 */ |
| 137 setRowHeight(height) { |
| 138 this._rowHeight = height; |
| 139 } |
| 140 |
| 141 /** |
| 142 * @param {number} height |
| 143 */ |
| 144 setHeaderHeight(height) { |
| 145 this._headerHeight = height; |
| 146 } |
| 147 |
| 148 /** |
| 149 * @param {number} padding |
| 150 */ |
| 151 setRightPadding(padding) { |
| 152 this._rightPadding = padding; |
| 153 this._calculateCanvasSize(); |
| 154 } |
| 155 |
| 156 /** |
| 157 * @param {!WebInspector.NetworkTimeCalculator} calculator |
| 158 */ |
| 159 setCalculator(calculator) { |
| 160 this._calculator = calculator; |
| 161 } |
| 162 |
| 163 /** |
| 164 * @param {number} x |
| 165 * @param {number} y |
| 166 * @return {?WebInspector.NetworkRequest} |
| 167 */ |
| 168 getRequestFromPoint(x, y) { |
| 169 return this._requestData[Math.floor((this._scrollTop + y - this._headerHeigh
t) / this._rowHeight)] || null; |
| 170 } |
| 171 |
| 172 scheduleDraw() { |
| 173 if (this._updateRequestID) |
| 174 return; |
| 175 this._updateRequestID = this.element.window().requestAnimationFrame(() => th
is.update()); |
| 176 } |
| 177 |
| 178 /** |
| 179 * @param {number=} scrollTop |
| 180 * @param {!Map<string, !Array<number>>=} eventDividers |
| 181 * @param {!{requests: !Array<!WebInspector.NetworkRequest>, navigationRequest
: ?WebInspector.NetworkRequest}=} requestData |
| 182 */ |
| 183 update(scrollTop, eventDividers, requestData) { |
| 184 if (scrollTop !== undefined) |
| 185 this._scrollTop = scrollTop; |
| 186 if (requestData) { |
| 187 this._requestData = requestData.requests; |
| 188 this._navigationRequest = requestData.navigationRequest; |
| 189 this._calculateCanvasSize(); |
| 190 } |
| 191 if (eventDividers !== undefined) |
| 192 this._eventDividers = eventDividers; |
| 193 this.element.window().cancelAnimationFrame(this._updateRequestID); |
| 194 this._updateRequestID = null; |
| 195 |
| 196 this._startTime = this._calculator.minimumBoundary(); |
| 197 this._endTime = this._calculator.maximumBoundary(); |
| 198 this._resetCanvas(); |
| 199 this._draw(); |
| 200 } |
| 201 |
| 202 _resetCanvas() { |
| 203 var ratio = window.devicePixelRatio; |
| 204 this._canvas.width = this._offsetWidth * ratio; |
| 205 this._canvas.height = this._offsetHeight * ratio; |
| 206 this._canvas.style.width = this._offsetWidth + 'px'; |
| 207 this._canvas.style.height = this._offsetHeight + 'px'; |
| 208 } |
| 209 |
| 210 /** |
| 211 * @override |
| 212 */ |
| 213 onResize() { |
| 214 super.onResize(); |
| 215 this._calculateCanvasSize(); |
| 216 this.scheduleDraw(); |
| 217 } |
| 218 |
| 219 _calculateCanvasSize() { |
| 220 this._offsetWidth = this.contentElement.offsetWidth - this._rightPadding; |
| 221 this._offsetHeight = this.contentElement.offsetHeight; |
| 222 this._calculator.setDisplayWindow(this._offsetWidth); |
| 223 this._canvasPosition = this._canvas.getBoundingClientRect(); |
| 224 } |
| 225 |
| 226 /** |
| 227 * @param {!WebInspector.RequestTimeRangeNames} type |
| 228 * @return {string} |
| 229 */ |
| 230 _colorForType(type) { |
| 231 var types = WebInspector.RequestTimeRangeNames; |
| 232 switch (type) { |
| 233 case types.Receiving: |
| 234 case types.ReceivingPush: |
| 235 return '#03A9F4'; |
| 236 case types.Waiting: |
| 237 return '#00C853'; |
| 238 case types.Connecting: |
| 239 return '#FF9800'; |
| 240 case types.SSL: |
| 241 return '#9C27B0'; |
| 242 case types.DNS: |
| 243 return '#009688'; |
| 244 case types.Proxy: |
| 245 return '#A1887F'; |
| 246 case types.Blocking: |
| 247 return '#AAAAAA'; |
| 248 case types.Push: |
| 249 return '#8CDBff'; |
| 250 case types.Queueing: |
| 251 return 'white'; |
| 252 case types.ServiceWorker: |
| 253 case types.ServiceWorkerPreparation: |
| 254 default: |
| 255 return 'orange'; |
| 256 } |
| 257 } |
| 258 |
| 259 /** |
| 260 * @param {number} time |
| 261 * @return {number} |
| 262 */ |
| 263 _timeToPosition(time) { |
| 264 var availableWidth = this._offsetWidth - this._leftPadding; |
| 265 var timeToPixel = availableWidth / (this._endTime - this._startTime); |
| 266 return Math.floor(this._leftPadding + (time - this._startTime) * timeToPixel
); |
| 267 } |
| 268 |
| 269 _draw() { |
| 270 var useTimingBars = |
| 271 !WebInspector.moduleSetting('networkColorCodeResourceTypes').get() && !t
his._calculator.startAtZero; |
| 272 var requests = this._requestData; |
| 273 var context = this._canvas.getContext('2d'); |
| 274 context.save(); |
| 275 context.scale(window.devicePixelRatio, window.devicePixelRatio); |
| 276 context.translate(0, this._headerHeight); |
| 277 context.rect(0, 0, this._offsetWidth, this._offsetHeight); |
| 278 context.clip(); |
| 279 var firstRequestIndex = Math.floor(this._scrollTop / this._rowHeight); |
| 280 var lastRequestIndex = |
| 281 Math.min(requests.length, firstRequestIndex + Math.ceil(this._offsetHeig
ht / this._rowHeight)); |
| 282 for (var i = firstRequestIndex; i < lastRequestIndex; i++) { |
| 283 var rowOffset = this._rowHeight * i; |
| 284 var request = requests[i]; |
| 285 this._decorateRow(context, request, i, rowOffset - this._scrollTop); |
| 286 if (useTimingBars) |
| 287 this._drawTimingBars(context, request, rowOffset - this._scrollTop); |
| 288 else |
| 289 this._drawSimplifiedBars(context, request, rowOffset - this._scrollTop); |
| 290 } |
| 291 this._drawEventDividers(context); |
| 292 context.restore(); |
| 293 |
| 294 const freeZoneAtLeft = 75; |
| 295 WebInspector.TimelineGrid.drawCanvasGrid(context, this._calculator, this._fo
ntSize, freeZoneAtLeft); |
| 296 } |
| 297 |
| 298 /** |
| 299 * @param {!CanvasRenderingContext2D} context |
| 300 */ |
| 301 _drawEventDividers(context) { |
| 302 context.save(); |
| 303 context.lineWidth = 1; |
| 304 for (var color of this._eventDividers.keys()) { |
| 305 context.strokeStyle = color; |
| 306 for (var time of this._eventDividers.get(color)) { |
| 307 context.beginPath(); |
| 308 var x = this._timeToPosition(time); |
| 309 context.moveTo(x, 0); |
| 310 context.lineTo(x, this._offsetHeight); |
| 311 } |
| 312 context.stroke(); |
| 313 } |
| 314 context.restore(); |
| 315 } |
| 316 |
| 317 /** |
| 318 * @return {number} |
| 319 */ |
| 320 _timelineDuration() { |
| 321 return this._calculator.maximumBoundary() - this._calculator.minimumBoundary
(); |
| 322 } |
| 323 |
| 324 /** |
| 325 * @param {!WebInspector.RequestTimeRangeNames=} type |
| 326 * @return {number} |
| 327 */ |
| 328 _getBarHeight(type) { |
| 329 var types = WebInspector.RequestTimeRangeNames; |
| 330 switch (type) { |
| 331 case types.Connecting: |
| 332 case types.SSL: |
| 333 case types.DNS: |
| 334 case types.Proxy: |
| 335 case types.Blocking: |
| 336 case types.Push: |
| 337 case types.Queueing: |
| 338 return 7; |
| 339 default: |
| 340 return 13; |
| 341 } |
| 342 } |
| 343 |
| 344 /** |
| 345 * @param {!WebInspector.NetworkRequest} request |
| 346 * @return {string} |
| 347 */ |
| 348 _borderColorForResourceType(request) { |
| 349 var resourceType = request.resourceType(); |
| 350 if (this._borderColorsForResourceTypeCache.has(resourceType)) |
| 351 return this._borderColorsForResourceTypeCache.get(resourceType); |
| 352 var colorsForResourceType = WebInspector.NetworkTimelineColumn._colorsForRes
ourceType; |
| 353 var color = colorsForResourceType[resourceType] || colorsForResourceType.Oth
er; |
| 354 var parsedColor = WebInspector.Color.parse(color); |
| 355 var hsla = parsedColor.hsla(); |
| 356 hsla[1] /= 2; |
| 357 hsla[2] -= Math.min(hsla[2], 0.2); |
| 358 var resultColor = /** @type {string} */ (parsedColor.asString(null)); |
| 359 this._borderColorsForResourceTypeCache.set(resourceType, resultColor); |
| 360 return resultColor; |
| 361 } |
| 362 |
| 363 /** |
| 364 * @param {!CanvasRenderingContext2D} context |
| 365 * @param {!WebInspector.NetworkRequest} request |
| 366 * @return {string|!CanvasGradient} |
| 367 */ |
| 368 _colorForResourceType(context, request) { |
| 369 var colorsForResourceType = WebInspector.NetworkTimelineColumn._colorsForRes
ourceType; |
| 370 var resourceType = request.resourceType(); |
| 371 var color = colorsForResourceType[resourceType] || colorsForResourceType.Oth
er; |
| 372 if (request.cached()) |
| 373 return color; |
| 374 |
| 375 if (this._colorsForResourceTypeCache.has(color)) |
| 376 return this._colorsForResourceTypeCache.get(color); |
| 377 var parsedColor = WebInspector.Color.parse(color); |
| 378 var hsla = parsedColor.hsla(); |
| 379 hsla[1] -= Math.min(hsla[1], 0.28); |
| 380 hsla[2] -= Math.min(hsla[2], 0.15); |
| 381 var gradient = context.createLinearGradient(0, 0, 0, this._getBarHeight()); |
| 382 gradient.addColorStop(0, color); |
| 383 gradient.addColorStop(1, /** @type {string} */ (parsedColor.asString(null)))
; |
| 384 this._colorsForResourceTypeCache.set(color, gradient); |
| 385 return gradient; |
| 386 } |
| 387 |
| 388 /** |
| 389 * @param {!CanvasRenderingContext2D} context |
| 390 * @param {!WebInspector.NetworkRequest} request |
| 391 * @param {number} y |
| 392 */ |
| 393 _drawSimplifiedBars(context, request, y) { |
| 394 /** @const */ |
| 395 var borderWidth = 1; |
| 396 |
| 397 context.save(); |
| 398 var percentages = this._calculator.computeBarGraphPercentages(request); |
| 399 var drawWidth = this._offsetWidth - this._leftPadding; |
| 400 var borderOffset = borderWidth % 2 === 0 ? 0 : .5; |
| 401 var start = this._leftPadding + Math.floor((percentages.start / 100) * drawW
idth) + borderOffset; |
| 402 var mid = this._leftPadding + Math.floor((percentages.middle / 100) * drawWi
dth) + borderOffset; |
| 403 var end = this._leftPadding + Math.floor((percentages.end / 100) * drawWidth
) + borderOffset; |
| 404 var height = this._getBarHeight(); |
| 405 y += Math.floor(this._rowHeight / 2 - height / 2 + borderWidth) - borderWidt
h / 2; |
| 406 |
| 407 context.translate(0, y); |
| 408 context.fillStyle = this._colorForResourceType(context, request); |
| 409 context.strokeStyle = this._borderColorForResourceType(request); |
| 410 context.lineWidth = borderWidth; |
| 411 |
| 412 context.beginPath(); |
| 413 context.globalAlpha = .5; |
| 414 context.rect(start, 0, mid - start, height - borderWidth); |
| 415 context.fill(); |
| 416 context.stroke(); |
| 417 |
| 418 var barWidth = Math.max(2, end - mid); |
| 419 context.beginPath(); |
| 420 context.globalAlpha = 1; |
| 421 context.rect(mid, 0, barWidth, height - borderWidth); |
| 422 context.fill(); |
| 423 context.stroke(); |
| 424 |
| 425 if (request === this._hoveredRequest) { |
| 426 var labels = this._calculator.computeBarGraphLabels(request); |
| 427 this._drawSimplifiedBarDetails(context, labels.left, labels.right, start,
mid, mid + barWidth + borderOffset); |
| 428 } |
| 429 |
| 430 context.restore(); |
| 431 } |
| 432 |
| 433 /** |
| 434 * @param {!CanvasRenderingContext2D} context |
| 435 * @param {string} leftText |
| 436 * @param {string} rightText |
| 437 * @param {number} startX |
| 438 * @param {number} midX |
| 439 * @param {number} endX |
| 440 */ |
| 441 _drawSimplifiedBarDetails(context, leftText, rightText, startX, midX, endX) { |
| 442 /** @const */ |
| 443 var barDotLineLength = 10; |
| 444 |
| 445 context.save(); |
| 446 var height = this._getBarHeight(); |
| 447 var leftLabelWidth = context.measureText(leftText).width; |
| 448 var rightLabelWidth = context.measureText(rightText).width; |
| 449 context.fillStyle = '#444'; |
| 450 context.strokeStyle = '#444'; |
| 451 if (leftLabelWidth < midX - startX) { |
| 452 var midBarX = startX + (midX - startX) / 2 - leftLabelWidth / 2; |
| 453 context.fillText(leftText, midBarX, this._fontSize); |
| 454 } else if (barDotLineLength + leftLabelWidth + this._leftPadding < startX) { |
| 455 context.beginPath(); |
| 456 context.arc(startX, Math.floor(height / 2), 2, 0, 2 * Math.PI); |
| 457 context.fill(); |
| 458 context.fillText(leftText, startX - leftLabelWidth - barDotLineLength - 1,
this._fontSize); |
| 459 context.beginPath(); |
| 460 context.lineWidth = 1; |
| 461 context.moveTo(startX - barDotLineLength, Math.floor(height / 2)); |
| 462 context.lineTo(startX, Math.floor(height / 2)); |
| 463 context.stroke(); |
| 464 } |
| 465 |
| 466 if (rightLabelWidth < endX - midX) { |
| 467 var midBarX = midX + (endX - midX) / 2 - rightLabelWidth / 2; |
| 468 context.fillText(rightText, midBarX, this._fontSize); |
| 469 } else if (endX + barDotLineLength + rightLabelWidth < this._offsetWidth - t
his._leftPadding) { |
| 470 context.beginPath(); |
| 471 context.arc(endX, Math.floor(height / 2), 2, 0, 2 * Math.PI); |
| 472 context.fill(); |
| 473 context.fillText(rightText, endX + barDotLineLength + 1, this._fontSize); |
| 474 context.beginPath(); |
| 475 context.lineWidth = 1; |
| 476 context.moveTo(endX, Math.floor(height / 2)); |
| 477 context.lineTo(endX + barDotLineLength, Math.floor(height / 2)); |
| 478 context.stroke(); |
| 479 } |
| 480 context.restore(); |
| 481 } |
| 482 |
| 483 /** |
| 484 * @param {!CanvasRenderingContext2D} context |
| 485 * @param {!WebInspector.NetworkRequest} request |
| 486 * @param {number} y |
| 487 */ |
| 488 _drawTimingBars(context, request, y) { |
| 489 context.save(); |
| 490 var ranges = WebInspector.RequestTimingView.calculateRequestTimeRanges(reque
st, 0); |
| 491 for (var range of ranges) { |
| 492 if (range.name === WebInspector.RequestTimeRangeNames.Total || |
| 493 range.name === WebInspector.RequestTimeRangeNames.Sending || range.end
- range.start === 0) |
| 494 continue; |
| 495 context.beginPath(); |
| 496 var lineWidth = 0; |
| 497 var color = this._colorForType(range.name); |
| 498 var borderColor = color; |
| 499 if (range.name === WebInspector.RequestTimeRangeNames.Queueing) { |
| 500 borderColor = 'lightgrey'; |
| 501 lineWidth = 2; |
| 502 } |
| 503 if (range.name === WebInspector.RequestTimeRangeNames.Receiving) |
| 504 lineWidth = 2; |
| 505 context.fillStyle = color; |
| 506 var height = this._getBarHeight(range.name); |
| 507 var middleBarY = y + Math.floor(this._rowHeight / 2 - height / 2) + lineWi
dth / 2; |
| 508 var start = this._timeToPosition(range.start); |
| 509 var end = this._timeToPosition(range.end); |
| 510 context.rect(start, middleBarY, end - start, height - lineWidth); |
| 511 if (lineWidth) { |
| 512 context.lineWidth = lineWidth; |
| 513 context.strokeStyle = borderColor; |
| 514 context.stroke(); |
| 515 } |
| 516 context.fill(); |
| 517 } |
| 518 context.restore(); |
| 519 } |
| 520 |
| 521 /** |
| 522 * @param {!CanvasRenderingContext2D} context |
| 523 * @param {!WebInspector.NetworkRequest} request |
| 524 * @param {number} rowNumber |
| 525 * @param {number} y |
| 526 */ |
| 527 _decorateRow(context, request, rowNumber, y) { |
| 528 if (rowNumber % 2 === 1 && this._hoveredRequest !== request && this._navigat
ionRequest !== request && |
| 529 !this._initiatorGraph) |
| 530 return; |
| 531 |
| 532 var color = getRowColor.call(this); |
| 533 if (color === 'transparent') |
| 534 return; |
| 535 context.save(); |
| 536 context.beginPath(); |
| 537 context.fillStyle = color; |
| 538 context.rect(0, y, this._offsetWidth, this._rowHeight); |
| 539 context.fill(); |
| 540 context.restore(); |
| 541 |
| 542 /** |
| 543 * @return {string} |
| 544 * @this {WebInspector.NetworkTimelineColumn} |
| 545 */ |
| 546 function getRowColor() { |
| 547 if (this._hoveredRequest === request) |
| 548 return this._rowHoverColor; |
| 549 if (this._initiatorGraph) { |
| 550 if (this._initiatorGraph.initiators.has(request)) |
| 551 return this._parentInitiatorColor; |
| 552 if (this._initiatorGraph.initiated.has(request)) |
| 553 return this._initiatedColor; |
| 554 } |
| 555 if (this._navigationRequest === request) |
| 556 return this._rowNavigationRequestColor; |
| 557 if (rowNumber % 2 === 1) |
| 558 return 'transparent'; |
| 559 return this._rowStripeColor; |
| 560 } |
| 561 } |
62 }; | 562 }; |
63 | 563 |
64 WebInspector.NetworkTimelineColumn._colorsForResourceType = { | 564 WebInspector.NetworkTimelineColumn._colorsForResourceType = { |
65 document: "hsl(215, 100%, 80%)", | 565 document: 'hsl(215, 100%, 80%)', |
66 font: "hsl(8, 100%, 80%)", | 566 font: 'hsl(8, 100%, 80%)', |
67 media: "hsl(272, 64%, 80%)", | 567 media: 'hsl(272, 64%, 80%)', |
68 image: "hsl(272, 64%, 80%)", | 568 image: 'hsl(272, 64%, 80%)', |
69 script: "hsl(31, 100%, 80%)", | 569 script: 'hsl(31, 100%, 80%)', |
70 stylesheet: "hsl(90, 50%, 80%)", | 570 stylesheet: 'hsl(90, 50%, 80%)', |
71 texttrack: "hsl(8, 100%, 80%)", | 571 texttrack: 'hsl(8, 100%, 80%)', |
72 websocket: "hsl(0, 0%, 95%)", | 572 websocket: 'hsl(0, 0%, 95%)', |
73 xhr: "hsl(53, 100%, 80%)", | 573 xhr: 'hsl(53, 100%, 80%)', |
74 other: "hsl(0, 0%, 95%)" | 574 other: 'hsl(0, 0%, 95%)' |
75 }; | 575 }; |
76 | |
77 WebInspector.NetworkTimelineColumn.prototype = { | |
78 /** | |
79 * @override | |
80 */ | |
81 willHide: function() | |
82 { | |
83 this._popoverHelper.hidePopover(); | |
84 }, | |
85 | |
86 /** | |
87 * @override | |
88 */ | |
89 wasShown: function() | |
90 { | |
91 this.update(); | |
92 }, | |
93 | |
94 /** | |
95 * @param {!Element} element | |
96 * @param {!Event} event | |
97 * @return {!AnchorBox|undefined} | |
98 */ | |
99 _getPopoverAnchor: function(element, event) | |
100 { | |
101 if (!this._hoveredRequest) | |
102 return; | |
103 | |
104 var range = WebInspector.RequestTimingView.calculateRequestTimeRanges(th
is._hoveredRequest, 0).find(data => data.name === "total"); | |
105 var start = this._timeToPosition(range.start); | |
106 var end = this._timeToPosition(range.end); | |
107 | |
108 if (event.clientX < this._canvasPosition.left + start || event.clientX >
this._canvasPosition.left + end) | |
109 return; | |
110 | |
111 var rowIndex = this._requestData.findIndex(request => this._hoveredReque
st === request); | |
112 var barHeight = this._getBarHeight(range.name); | |
113 var y = this._headerHeight + (this._rowHeight * rowIndex - this._scrollT
op) + ((this._rowHeight - barHeight) / 2); | |
114 | |
115 if (event.clientY < this._canvasPosition.top + y || event.clientY > this
._canvasPosition.top + y + barHeight) | |
116 return; | |
117 | |
118 var anchorBox = this.element.boxInWindow(); | |
119 anchorBox.x += start; | |
120 anchorBox.y += y; | |
121 anchorBox.width = end - start; | |
122 anchorBox.height = barHeight; | |
123 return anchorBox; | |
124 }, | |
125 | |
126 /** | |
127 * @param {!Element|!AnchorBox} anchor | |
128 * @param {!WebInspector.Popover} popover | |
129 */ | |
130 _showPopover: function(anchor, popover) | |
131 { | |
132 if (!this._hoveredRequest) | |
133 return; | |
134 var content = WebInspector.RequestTimingView.createTimingTable(this._hov
eredRequest, this._calculator.minimumBoundary()); | |
135 popover.showForAnchor(content, anchor); | |
136 }, | |
137 | |
138 /** | |
139 * @param {?WebInspector.NetworkRequest} request | |
140 * @param {boolean} highlightInitiatorChain | |
141 */ | |
142 setHoveredRequest: function(request, highlightInitiatorChain) | |
143 { | |
144 this._hoveredRequest = request; | |
145 this._initiatorGraph = (highlightInitiatorChain && request) ? request.in
itiatorGraph() : null; | |
146 this.update(); | |
147 }, | |
148 | |
149 /** | |
150 * @param {number} height | |
151 */ | |
152 setRowHeight: function(height) | |
153 { | |
154 this._rowHeight = height; | |
155 }, | |
156 | |
157 /** | |
158 * @param {number} height | |
159 */ | |
160 setHeaderHeight: function(height) | |
161 { | |
162 this._headerHeight = height; | |
163 }, | |
164 | |
165 /** | |
166 * @param {number} padding | |
167 */ | |
168 setRightPadding: function(padding) | |
169 { | |
170 this._rightPadding = padding; | |
171 this._calculateCanvasSize(); | |
172 }, | |
173 | |
174 /** | |
175 * @param {!WebInspector.NetworkTimeCalculator} calculator | |
176 */ | |
177 setCalculator: function(calculator) | |
178 { | |
179 this._calculator = calculator; | |
180 }, | |
181 | |
182 /** | |
183 * @param {number} x | |
184 * @param {number} y | |
185 * @return {?WebInspector.NetworkRequest} | |
186 */ | |
187 getRequestFromPoint: function(x, y) | |
188 { | |
189 return this._requestData[Math.floor((this._scrollTop + y - this._headerH
eight) / this._rowHeight)] || null; | |
190 }, | |
191 | |
192 scheduleDraw: function() | |
193 { | |
194 if (this._updateRequestID) | |
195 return; | |
196 this._updateRequestID = this.element.window().requestAnimationFrame(() =
> this.update()); | |
197 }, | |
198 | |
199 /** | |
200 * @param {number=} scrollTop | |
201 * @param {!Map<string, !Array<number>>=} eventDividers | |
202 * @param {!{requests: !Array<!WebInspector.NetworkRequest>, navigationReque
st: ?WebInspector.NetworkRequest}=} requestData | |
203 */ | |
204 update: function(scrollTop, eventDividers, requestData) | |
205 { | |
206 if (scrollTop !== undefined) | |
207 this._scrollTop = scrollTop; | |
208 if (requestData) { | |
209 this._requestData = requestData.requests; | |
210 this._navigationRequest = requestData.navigationRequest; | |
211 this._calculateCanvasSize(); | |
212 } | |
213 if (eventDividers !== undefined) | |
214 this._eventDividers = eventDividers; | |
215 this.element.window().cancelAnimationFrame(this._updateRequestID); | |
216 this._updateRequestID = null; | |
217 | |
218 this._startTime = this._calculator.minimumBoundary(); | |
219 this._endTime = this._calculator.maximumBoundary(); | |
220 this._resetCanvas(); | |
221 this._draw(); | |
222 }, | |
223 | |
224 _resetCanvas: function() | |
225 { | |
226 var ratio = window.devicePixelRatio; | |
227 this._canvas.width = this._offsetWidth * ratio; | |
228 this._canvas.height = this._offsetHeight * ratio; | |
229 this._canvas.style.width = this._offsetWidth + "px"; | |
230 this._canvas.style.height = this._offsetHeight + "px"; | |
231 }, | |
232 | |
233 /** | |
234 * @override | |
235 */ | |
236 onResize: function() | |
237 { | |
238 WebInspector.VBox.prototype.onResize.call(this); | |
239 this._calculateCanvasSize(); | |
240 this.scheduleDraw(); | |
241 }, | |
242 | |
243 _calculateCanvasSize: function() | |
244 { | |
245 this._offsetWidth = this.contentElement.offsetWidth - this._rightPadding
; | |
246 this._offsetHeight = this.contentElement.offsetHeight; | |
247 this._calculator.setDisplayWindow(this._offsetWidth); | |
248 this._canvasPosition = this._canvas.getBoundingClientRect(); | |
249 }, | |
250 | |
251 /** | |
252 * @param {!WebInspector.RequestTimeRangeNames} type | |
253 * @return {string} | |
254 */ | |
255 _colorForType: function(type) | |
256 { | |
257 var types = WebInspector.RequestTimeRangeNames; | |
258 switch (type) { | |
259 case types.Receiving: | |
260 case types.ReceivingPush: | |
261 return "#03A9F4"; | |
262 case types.Waiting: | |
263 return "#00C853"; | |
264 case types.Connecting: | |
265 return "#FF9800"; | |
266 case types.SSL: | |
267 return "#9C27B0"; | |
268 case types.DNS: | |
269 return "#009688"; | |
270 case types.Proxy: | |
271 return "#A1887F"; | |
272 case types.Blocking: | |
273 return "#AAAAAA"; | |
274 case types.Push: | |
275 return "#8CDBff"; | |
276 case types.Queueing: | |
277 return "white"; | |
278 case types.ServiceWorker: | |
279 case types.ServiceWorkerPreparation: | |
280 default: | |
281 return "orange"; | |
282 } | |
283 }, | |
284 | |
285 /** | |
286 * @param {number} time | |
287 * @return {number} | |
288 */ | |
289 _timeToPosition: function(time) | |
290 { | |
291 var availableWidth = this._offsetWidth - this._leftPadding; | |
292 var timeToPixel = availableWidth / (this._endTime - this._startTime); | |
293 return Math.floor(this._leftPadding + (time - this._startTime) * timeToP
ixel); | |
294 }, | |
295 | |
296 _draw: function() | |
297 { | |
298 var useTimingBars = !WebInspector.moduleSetting("networkColorCodeResourc
eTypes").get() && !this._calculator.startAtZero; | |
299 var requests = this._requestData; | |
300 var context = this._canvas.getContext("2d"); | |
301 context.save(); | |
302 context.scale(window.devicePixelRatio, window.devicePixelRatio); | |
303 context.translate(0, this._headerHeight); | |
304 context.rect(0, 0, this._offsetWidth, this._offsetHeight); | |
305 context.clip(); | |
306 var firstRequestIndex = Math.floor(this._scrollTop / this._rowHeight); | |
307 var lastRequestIndex = Math.min(requests.length, firstRequestIndex + Mat
h.ceil(this._offsetHeight / this._rowHeight)); | |
308 for (var i = firstRequestIndex; i < lastRequestIndex; i++) { | |
309 var rowOffset = this._rowHeight * i; | |
310 var request = requests[i]; | |
311 this._decorateRow(context, request, i, rowOffset - this._scrollTop); | |
312 if (useTimingBars) | |
313 this._drawTimingBars(context, request, rowOffset - this._scrollT
op); | |
314 else | |
315 this._drawSimplifiedBars(context, request, rowOffset - this._scr
ollTop); | |
316 } | |
317 this._drawEventDividers(context); | |
318 context.restore(); | |
319 | |
320 const freeZoneAtLeft = 75; | |
321 WebInspector.TimelineGrid.drawCanvasGrid(context, this._calculator, this
._fontSize, freeZoneAtLeft); | |
322 }, | |
323 | |
324 /** | |
325 * @param {!CanvasRenderingContext2D} context | |
326 */ | |
327 _drawEventDividers: function(context) | |
328 { | |
329 context.save(); | |
330 context.lineWidth = 1; | |
331 for (var color of this._eventDividers.keys()) { | |
332 context.strokeStyle = color; | |
333 for (var time of this._eventDividers.get(color)) { | |
334 context.beginPath(); | |
335 var x = this._timeToPosition(time); | |
336 context.moveTo(x, 0); | |
337 context.lineTo(x, this._offsetHeight); | |
338 } | |
339 context.stroke(); | |
340 } | |
341 context.restore(); | |
342 }, | |
343 | |
344 /** | |
345 * @return {number} | |
346 */ | |
347 _timelineDuration: function() | |
348 { | |
349 return this._calculator.maximumBoundary() - this._calculator.minimumBoun
dary(); | |
350 }, | |
351 | |
352 /** | |
353 * @param {!WebInspector.RequestTimeRangeNames=} type | |
354 * @return {number} | |
355 */ | |
356 _getBarHeight: function(type) | |
357 { | |
358 var types = WebInspector.RequestTimeRangeNames; | |
359 switch (type) { | |
360 case types.Connecting: | |
361 case types.SSL: | |
362 case types.DNS: | |
363 case types.Proxy: | |
364 case types.Blocking: | |
365 case types.Push: | |
366 case types.Queueing: | |
367 return 7; | |
368 default: | |
369 return 13; | |
370 } | |
371 }, | |
372 | |
373 /** | |
374 * @param {!WebInspector.NetworkRequest} request | |
375 * @return {string} | |
376 */ | |
377 _borderColorForResourceType: function(request) | |
378 { | |
379 var resourceType = request.resourceType(); | |
380 if (this._borderColorsForResourceTypeCache.has(resourceType)) | |
381 return this._borderColorsForResourceTypeCache.get(resourceType); | |
382 var colorsForResourceType = WebInspector.NetworkTimelineColumn._colorsFo
rResourceType; | |
383 var color = colorsForResourceType[resourceType] || colorsForResourceType
.Other; | |
384 var parsedColor = WebInspector.Color.parse(color); | |
385 var hsla = parsedColor.hsla(); | |
386 hsla[1] /= 2; | |
387 hsla[2] -= Math.min(hsla[2], 0.2); | |
388 var resultColor = /** @type {string} */ (parsedColor.asString(null)); | |
389 this._borderColorsForResourceTypeCache.set(resourceType, resultColor); | |
390 return resultColor; | |
391 }, | |
392 | |
393 /** | |
394 * @param {!CanvasRenderingContext2D} context | |
395 * @param {!WebInspector.NetworkRequest} request | |
396 * @return {string|!CanvasGradient} | |
397 */ | |
398 _colorForResourceType: function(context, request) | |
399 { | |
400 var colorsForResourceType = WebInspector.NetworkTimelineColumn._colorsFo
rResourceType; | |
401 var resourceType = request.resourceType(); | |
402 var color = colorsForResourceType[resourceType] || colorsForResourceType
.Other; | |
403 if (request.cached()) | |
404 return color; | |
405 | |
406 if (this._colorsForResourceTypeCache.has(color)) | |
407 return this._colorsForResourceTypeCache.get(color); | |
408 var parsedColor = WebInspector.Color.parse(color); | |
409 var hsla = parsedColor.hsla(); | |
410 hsla[1] -= Math.min(hsla[1], 0.28); | |
411 hsla[2] -= Math.min(hsla[2], 0.15); | |
412 var gradient = context.createLinearGradient(0, 0, 0, this._getBarHeight(
)); | |
413 gradient.addColorStop(0, color); | |
414 gradient.addColorStop(1, /** @type {string} */ (parsedColor.asString(nul
l))); | |
415 this._colorsForResourceTypeCache.set(color, gradient); | |
416 return gradient; | |
417 }, | |
418 | |
419 /** | |
420 * @param {!CanvasRenderingContext2D} context | |
421 * @param {!WebInspector.NetworkRequest} request | |
422 * @param {number} y | |
423 */ | |
424 _drawSimplifiedBars: function(context, request, y) | |
425 { | |
426 /** @const */ | |
427 var borderWidth = 1; | |
428 | |
429 context.save(); | |
430 var percentages = this._calculator.computeBarGraphPercentages(request); | |
431 var drawWidth = this._offsetWidth - this._leftPadding; | |
432 var borderOffset = borderWidth % 2 === 0 ? 0 : .5; | |
433 var start = this._leftPadding + Math.floor((percentages.start / 100) * d
rawWidth) + borderOffset; | |
434 var mid = this._leftPadding + Math.floor((percentages.middle / 100) * dr
awWidth) + borderOffset; | |
435 var end = this._leftPadding + Math.floor((percentages.end / 100) * drawW
idth) + borderOffset; | |
436 var height = this._getBarHeight(); | |
437 y += Math.floor(this._rowHeight / 2 - height / 2 + borderWidth) - border
Width / 2; | |
438 | |
439 context.translate(0, y); | |
440 context.fillStyle = this._colorForResourceType(context, request); | |
441 context.strokeStyle = this._borderColorForResourceType(request); | |
442 context.lineWidth = borderWidth; | |
443 | |
444 context.beginPath(); | |
445 context.globalAlpha = .5; | |
446 context.rect(start, 0, mid - start, height - borderWidth); | |
447 context.fill(); | |
448 context.stroke(); | |
449 | |
450 var barWidth = Math.max(2, end - mid); | |
451 context.beginPath(); | |
452 context.globalAlpha = 1; | |
453 context.rect(mid, 0, barWidth, height - borderWidth); | |
454 context.fill(); | |
455 context.stroke(); | |
456 | |
457 if (request === this._hoveredRequest) { | |
458 var labels = this._calculator.computeBarGraphLabels(request); | |
459 this._drawSimplifiedBarDetails(context, labels.left, labels.right, s
tart, mid, mid + barWidth + borderOffset); | |
460 } | |
461 | |
462 context.restore(); | |
463 }, | |
464 | |
465 /** | |
466 * @param {!CanvasRenderingContext2D} context | |
467 * @param {string} leftText | |
468 * @param {string} rightText | |
469 * @param {number} startX | |
470 * @param {number} midX | |
471 * @param {number} endX | |
472 */ | |
473 _drawSimplifiedBarDetails: function(context, leftText, rightText, startX, mi
dX, endX) | |
474 { | |
475 /** @const */ | |
476 var barDotLineLength = 10; | |
477 | |
478 context.save(); | |
479 var height = this._getBarHeight(); | |
480 var leftLabelWidth = context.measureText(leftText).width; | |
481 var rightLabelWidth = context.measureText(rightText).width; | |
482 context.fillStyle = "#444"; | |
483 context.strokeStyle = "#444"; | |
484 if (leftLabelWidth < midX - startX) { | |
485 var midBarX = startX + (midX - startX) / 2 - leftLabelWidth / 2; | |
486 context.fillText(leftText, midBarX, this._fontSize); | |
487 } else if (barDotLineLength + leftLabelWidth + this._leftPadding < start
X) { | |
488 context.beginPath(); | |
489 context.arc(startX, Math.floor(height / 2), 2, 0, 2 * Math.PI); | |
490 context.fill(); | |
491 context.fillText(leftText, startX - leftLabelWidth - barDotLineLengt
h - 1, this._fontSize); | |
492 context.beginPath(); | |
493 context.lineWidth = 1; | |
494 context.moveTo(startX - barDotLineLength, Math.floor(height / 2)); | |
495 context.lineTo(startX, Math.floor(height / 2)); | |
496 context.stroke(); | |
497 } | |
498 | |
499 if (rightLabelWidth < endX - midX) { | |
500 var midBarX = midX + (endX - midX) / 2 - rightLabelWidth / 2; | |
501 context.fillText(rightText, midBarX, this._fontSize); | |
502 } else if (endX + barDotLineLength + rightLabelWidth < this._offsetWidth
- this._leftPadding) { | |
503 context.beginPath(); | |
504 context.arc(endX, Math.floor(height / 2), 2, 0, 2 * Math.PI); | |
505 context.fill(); | |
506 context.fillText(rightText, endX + barDotLineLength + 1, this._fontS
ize); | |
507 context.beginPath(); | |
508 context.lineWidth = 1; | |
509 context.moveTo(endX, Math.floor(height / 2)); | |
510 context.lineTo(endX + barDotLineLength, Math.floor(height / 2)); | |
511 context.stroke(); | |
512 } | |
513 context.restore(); | |
514 }, | |
515 | |
516 /** | |
517 * @param {!CanvasRenderingContext2D} context | |
518 * @param {!WebInspector.NetworkRequest} request | |
519 * @param {number} y | |
520 */ | |
521 _drawTimingBars: function(context, request, y) | |
522 { | |
523 context.save(); | |
524 var ranges = WebInspector.RequestTimingView.calculateRequestTimeRanges(r
equest, 0); | |
525 for (var range of ranges) { | |
526 if (range.name === WebInspector.RequestTimeRangeNames.Total || | |
527 range.name === WebInspector.RequestTimeRangeNames.Sending || | |
528 range.end - range.start === 0) | |
529 continue; | |
530 context.beginPath(); | |
531 var lineWidth = 0; | |
532 var color = this._colorForType(range.name); | |
533 var borderColor = color; | |
534 if (range.name === WebInspector.RequestTimeRangeNames.Queueing) { | |
535 borderColor = "lightgrey"; | |
536 lineWidth = 2; | |
537 } | |
538 if (range.name === WebInspector.RequestTimeRangeNames.Receiving) | |
539 lineWidth = 2; | |
540 context.fillStyle = color; | |
541 var height = this._getBarHeight(range.name); | |
542 var middleBarY = y + Math.floor(this._rowHeight / 2 - height / 2) +
lineWidth / 2; | |
543 var start = this._timeToPosition(range.start); | |
544 var end = this._timeToPosition(range.end); | |
545 context.rect(start, middleBarY, end - start, height - lineWidth); | |
546 if (lineWidth) { | |
547 context.lineWidth = lineWidth; | |
548 context.strokeStyle = borderColor; | |
549 context.stroke(); | |
550 } | |
551 context.fill(); | |
552 } | |
553 context.restore(); | |
554 }, | |
555 | |
556 /** | |
557 * @param {!CanvasRenderingContext2D} context | |
558 * @param {!WebInspector.NetworkRequest} request | |
559 * @param {number} rowNumber | |
560 * @param {number} y | |
561 */ | |
562 _decorateRow: function(context, request, rowNumber, y) | |
563 { | |
564 if (rowNumber % 2 === 1 && this._hoveredRequest !== request && this._nav
igationRequest !== request && !this._initiatorGraph) | |
565 return; | |
566 | |
567 var color = getRowColor.call(this); | |
568 if (color === "transparent") | |
569 return; | |
570 context.save(); | |
571 context.beginPath(); | |
572 context.fillStyle = color; | |
573 context.rect(0, y, this._offsetWidth, this._rowHeight); | |
574 context.fill(); | |
575 context.restore(); | |
576 | |
577 /** | |
578 * @return {string} | |
579 * @this {WebInspector.NetworkTimelineColumn} | |
580 */ | |
581 function getRowColor() | |
582 { | |
583 if (this._hoveredRequest === request) | |
584 return this._rowHoverColor; | |
585 if (this._initiatorGraph) { | |
586 if (this._initiatorGraph.initiators.has(request)) | |
587 return this._parentInitiatorColor; | |
588 if (this._initiatorGraph.initiated.has(request)) | |
589 return this._initiatedColor; | |
590 } | |
591 if (this._navigationRequest === request) | |
592 return this._rowNavigationRequestColor; | |
593 if (rowNumber % 2 === 1) | |
594 return "transparent"; | |
595 return this._rowStripeColor; | |
596 } | |
597 }, | |
598 | |
599 __proto__: WebInspector.VBox.prototype | |
600 }; | |
OLD | NEW |