@@ -108,7 +108,7 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
108108 // When the user checks the "use same widths for each stack" checkbox, some
109109 // expensive computation happens when the canvas is drawn. These computations
110110 // can be reused for hit testing, and therefore are saved in these variables.
111- _sameWidthsIndexAtStart : null | number ;
111+ _sameWidthsIndexAtViewportStart : null | number ;
112112 _sameWidthsRangeLength : null | number ;
113113
114114 componentDidUpdate ( prevProps ) {
@@ -254,20 +254,30 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
254254
255255 // Compute the start index as well as the length for the "same width"
256256 // drawing as well, if needed.
257- this . _sameWidthsRangeLength = this . _sameWidthsIndexAtStart = null ;
257+ this . _sameWidthsRangeLength = this . _sameWidthsIndexAtViewportStart = null ;
258+ let sameWidthsIndexAtCanvasStart = null ;
259+ let sameWidthsIndexAtCanvasEnd = null ;
258260 if ( useStackChartSameWidths ) {
259- const sameWidthsIndexAtStart = Math . max (
261+ const sameWidthsIndexAtViewportStart = Math . max (
260262 0 ,
261263 bisectionRight ( sameWidthsIndexToTimestampMap , timeAtViewportStart ) - 1
262264 ) ;
263- const sameWidthsIndexAtEnd = Math . min (
265+ const sameWidthsIndexAtViewportEnd = Math . min (
264266 sameWidthsIndexToTimestampMap . length - 1 ,
265267 bisectionLeft ( sameWidthsIndexToTimestampMap , timeAtViewportEnd )
266268 ) ;
267269
268- this . _sameWidthsIndexAtStart = sameWidthsIndexAtStart ;
270+ this . _sameWidthsIndexAtViewportStart = sameWidthsIndexAtViewportStart ;
269271 this . _sameWidthsRangeLength =
270- sameWidthsIndexAtEnd - sameWidthsIndexAtStart ;
272+ sameWidthsIndexAtViewportEnd - sameWidthsIndexAtViewportStart ;
273+
274+ sameWidthsIndexAtCanvasStart =
275+ sameWidthsIndexAtViewportStart -
276+ ( marginLeft / innerContainerWidth ) * this . _sameWidthsRangeLength ;
277+ sameWidthsIndexAtCanvasEnd =
278+ sameWidthsIndexAtViewportEnd +
279+ ( TIMELINE_MARGIN_RIGHT / innerContainerWidth ) *
280+ this . _sameWidthsRangeLength ;
271281 }
272282
273283 const pixelAtViewportPosition = (
@@ -307,161 +317,175 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
307317 let lastDrawnPixelX = 0 ;
308318 for ( let i = 0 ; i < stackTiming . length ; i ++ ) {
309319 // Only draw boxes that overlap with the canvas.
320+ const isTimingBoxBeforeCanvas =
321+ useStackChartSameWidths &&
322+ stackTiming . sameWidthsEnd &&
323+ sameWidthsIndexAtCanvasStart !== null
324+ ? stackTiming . sameWidthsEnd [ i ] < sameWidthsIndexAtCanvasStart
325+ : stackTiming . end [ i ] < timeAtStart ;
326+ if ( isTimingBoxBeforeCanvas ) {
327+ continue ;
328+ }
329+
330+ const isTimingBoxAfterCanvas =
331+ useStackChartSameWidths &&
332+ stackTiming . sameWidthsStart &&
333+ sameWidthsIndexAtCanvasEnd !== null
334+ ? stackTiming . sameWidthsStart [ i ] > sameWidthsIndexAtCanvasEnd
335+ : stackTiming . start [ i ] > timeAtEnd ;
336+ if ( isTimingBoxAfterCanvas ) {
337+ break ;
338+ }
339+
340+ // Draw a box, but increase the size by a small portion in order to draw
341+ // a single pixel at the end with a slight opacity.
342+ //
343+ // Legend:
344+ // |======| A stack frame's timing.
345+ // |O| A single fully opaque pixel.
346+ // |.| A slightly transparent pixel.
347+ // | | A fully transparent pixel.
348+ //
349+ // Drawing strategy:
350+ //
351+ // Frame timing |=====||========| |=====| |=| |=|=|=|=|
352+ // Device Pixels |O|O|.|O|O|O|O|.| | |O|O|O|.| | |O|.| | |O|.|O|.|
353+ // CSS Pixels | | | | | | | | | | | | |
354+
355+ // First compute the left and right sides of the box.
356+ let floatX : DevicePixels ;
357+ let floatW : DevicePixels ;
310358 if (
311- stackTiming . end [ i ] > timeAtStart &&
312- stackTiming . start [ i ] < timeAtEnd
359+ useStackChartSameWidths &&
360+ stackTiming . sameWidthsStart &&
361+ stackTiming . sameWidthsEnd &&
362+ this . _sameWidthsRangeLength !== null &&
363+ this . _sameWidthsIndexAtViewportStart !== null
313364 ) {
314- // Draw a box, but increase the size by a small portion in order to draw
315- // a single pixel at the end with a slight opacity.
316- //
317- // Legend:
318- // |======| A stack frame's timing.
319- // |O| A single fully opaque pixel.
320- // |.| A slightly transparent pixel.
321- // | | A fully transparent pixel.
322- //
323- // Drawing strategy:
324- //
325- // Frame timing |=====||========| |=====| |=| |=|=|=|=|
326- // Device Pixels |O|O|.|O|O|O|O|.| | |O|O|O|.| | |O|.| | |O|.|O|.|
327- // CSS Pixels | | | | | | | | | | | | |
328-
329- // First compute the left and right sides of the box.
330- let floatX : DevicePixels ;
331- let floatW : DevicePixels ;
332- if (
333- useStackChartSameWidths &&
334- stackTiming . sameWidthsStart &&
335- stackTiming . sameWidthsEnd &&
336- this . _sameWidthsRangeLength !== null &&
337- this . _sameWidthsIndexAtStart !== null
338- ) {
339- floatX =
340- cssToDeviceScale *
341- ( marginLeft +
342- ( innerContainerWidth *
343- ( stackTiming . sameWidthsStart [ i ] -
344- this . _sameWidthsIndexAtStart ) ) /
345- this . _sameWidthsRangeLength ) ;
346- floatW =
347- ( innerDevicePixelsWidth *
348- ( stackTiming . sameWidthsEnd [ i ] -
349- stackTiming . sameWidthsStart [ i ] ) ) /
350- this . _sameWidthsRangeLength -
351- 1 ;
352- } else {
353- const viewportAtStartTime : UnitIntervalOfProfileRange =
354- ( stackTiming . start [ i ] - rangeStart ) / rangeLength ;
355- const viewportAtEndTime : UnitIntervalOfProfileRange =
356- ( stackTiming . end [ i ] - rangeStart ) / rangeLength ;
357- floatX = pixelAtViewportPosition ( viewportAtStartTime ) ;
358- floatW =
359- ( ( viewportAtEndTime - viewportAtStartTime ) *
360- innerDevicePixelsWidth ) /
361- viewportLength -
362- 1 ;
363- }
364-
365- // Determine if there is enough pixel space to draw this box, and snap the
366- // box to the pixels.
367- let snappedFloatX = floatX ;
368- let snappedFloatW = floatW ;
369- let skipDraw = true ;
370- if ( floatX >= lastDrawnPixelX ) {
371- // The x value is past the last lastDrawnPixelX, so it can be drawn.
372- skipDraw = false ;
373- } else if ( floatX + floatW > lastDrawnPixelX ) {
374- // The left side of the box is before the lastDrawnPixelX value, but the
375- // right hand side is within a range to be drawn. Truncate the box a little
376- // bit in order to draw it to the screen in the free space.
377- snappedFloatW = floatW - ( lastDrawnPixelX - floatX ) ;
378- snappedFloatX = lastDrawnPixelX ;
379- skipDraw = false ;
380- }
365+ floatX =
366+ cssToDeviceScale *
367+ ( marginLeft +
368+ ( innerContainerWidth *
369+ ( stackTiming . sameWidthsStart [ i ] -
370+ this . _sameWidthsIndexAtViewportStart ) ) /
371+ this . _sameWidthsRangeLength ) ;
372+ floatW =
373+ ( innerDevicePixelsWidth *
374+ ( stackTiming . sameWidthsEnd [ i ] - stackTiming . sameWidthsStart [ i ] ) ) /
375+ this . _sameWidthsRangeLength -
376+ 1 ;
377+ } else {
378+ const viewportAtStartTime : UnitIntervalOfProfileRange =
379+ ( stackTiming . start [ i ] - rangeStart ) / rangeLength ;
380+ const viewportAtEndTime : UnitIntervalOfProfileRange =
381+ ( stackTiming . end [ i ] - rangeStart ) / rangeLength ;
382+ floatX = pixelAtViewportPosition ( viewportAtStartTime ) ;
383+ floatW =
384+ ( ( viewportAtEndTime - viewportAtStartTime ) *
385+ innerDevicePixelsWidth ) /
386+ viewportLength -
387+ 1 ;
388+ }
381389
382- if ( skipDraw ) {
383- // This box didn't satisfy the constraints in the above if checks, so skip it.
384- continue ;
385- }
390+ // Determine if there is enough pixel space to draw this box, and snap the
391+ // box to the pixels.
392+ let snappedFloatX = floatX ;
393+ let snappedFloatW = floatW ;
394+ let skipDraw = true ;
395+ if ( floatX >= lastDrawnPixelX ) {
396+ // The x value is past the last lastDrawnPixelX, so it can be drawn.
397+ skipDraw = false ;
398+ } else if ( floatX + floatW > lastDrawnPixelX ) {
399+ // The left side of the box is before the lastDrawnPixelX value, but the
400+ // right hand side is within a range to be drawn. Truncate the box a little
401+ // bit in order to draw it to the screen in the free space.
402+ snappedFloatW = floatW - ( lastDrawnPixelX - floatX ) ;
403+ snappedFloatX = lastDrawnPixelX ;
404+ skipDraw = false ;
405+ }
386406
387- // Convert or compute all of the integer values for drawing the box.
388- // Note, this should all be Math.round instead of floor and ceil, but some
389- // off by one errors appear to be creating gaps where there shouldn't be any.
390- const intX = Math . floor ( snappedFloatX ) ;
391- const intY = Math . round (
392- depth * rowDevicePixelsHeight - viewportDevicePixelsTop
393- ) ;
394- const intW = Math . ceil ( Math . max ( 1 , snappedFloatW ) ) ;
395- const intH = Math . round (
396- rowDevicePixelsHeight - oneCssPixelInDevicePixels
397- ) ;
398-
399- // Look up information about this stack frame.
400- let text , category , isSelected ;
401- if ( stackTiming . callNode ) {
402- const callNodeIndex = stackTiming . callNode [ i ] ;
403- const funcIndex = callNodeTable . func [ callNodeIndex ] ;
404- const funcNameIndex = thread . funcTable . name [ funcIndex ] ;
405- text = thread . stringTable . getString ( funcNameIndex ) ;
406- const categoryIndex = callNodeTable . category [ callNodeIndex ] ;
407- category = categories [ categoryIndex ] ;
408- isSelected = selectedCallNodeIndex === callNodeIndex ;
409- } else {
410- const markerIndex = stackTiming . index [ i ] ;
411- const markerPayload = ( ( getMarker ( markerIndex )
412- . data : any ) : UserTimingMarkerPayload ) ;
413- text = markerPayload . name ;
414- category = categories [ categoryForUserTiming ] ;
415- isSelected = selectedCallNodeIndex === markerIndex ;
416- }
407+ if ( skipDraw ) {
408+ // This box didn't satisfy the constraints in the above if checks, so skip it.
409+ continue ;
410+ }
417411
418- const isHovered =
419- hoveredItem &&
420- depth === hoveredItem . depth &&
421- i === hoveredItem . stackTimingIndex ;
422-
423- const colorStyles = mapCategoryColorNameToStackChartStyles (
424- category . color
425- ) ;
426- // Draw the box.
427- fastFillStyle . set (
428- isHovered || isSelected
429- ? colorStyles . selectedFillStyle
430- : colorStyles . unselectedFillStyle
431- ) ;
432- ctx . fillRect (
433- intX ,
434- intY ,
435- // Add on a bit of BORDER_OPACITY to the end of the width, to draw a partial
436- // pixel. This will effectively draw a transparent version of the fill color
437- // without having to change the fill color. At the time of this writing it
438- // was the same performance cost as only providing integer values here.
439- intW + BORDER_OPACITY ,
440- intH
441- ) ;
442- lastDrawnPixelX =
443- intX +
444- intW +
445- // The border on the right is 1 device pixel wide.
446- 1 ;
412+ // Convert or compute all of the integer values for drawing the box.
413+ // Note, this should all be Math.round instead of floor and ceil, but some
414+ // off by one errors appear to be creating gaps where there shouldn't be any.
415+ const intX = Math . floor ( snappedFloatX ) ;
416+ const intY = Math . round (
417+ depth * rowDevicePixelsHeight - viewportDevicePixelsTop
418+ ) ;
419+ const intW = Math . ceil ( Math . max ( 1 , snappedFloatW ) ) ;
420+ const intH = Math . round (
421+ rowDevicePixelsHeight - oneCssPixelInDevicePixels
422+ ) ;
423+
424+ // Look up information about this stack frame.
425+ let text , category , isSelected ;
426+ if ( stackTiming . callNode ) {
427+ const callNodeIndex = stackTiming . callNode [ i ] ;
428+ const funcIndex = callNodeTable . func [ callNodeIndex ] ;
429+ const funcNameIndex = thread . funcTable . name [ funcIndex ] ;
430+ text = thread . stringTable . getString ( funcNameIndex ) ;
431+ const categoryIndex = callNodeTable . category [ callNodeIndex ] ;
432+ category = categories [ categoryIndex ] ;
433+ isSelected = selectedCallNodeIndex === callNodeIndex ;
434+ } else {
435+ const markerIndex = stackTiming . index [ i ] ;
436+ const markerPayload = ( ( getMarker ( markerIndex )
437+ . data : any ) : UserTimingMarkerPayload ) ;
438+ text = markerPayload . name ;
439+ category = categories [ categoryForUserTiming ] ;
440+ isSelected = selectedCallNodeIndex === markerIndex ;
441+ }
447442
448- // Draw the text label if it fits. Use the original float values here so that
449- // the text doesn't snap around when moving. Only the boxes should snap.
450- const textX : DevicePixels =
451- // Constrain the x coordinate to the leftmost area.
452- Math . max ( floatX , 0 ) + textDevicePixelsOffsetStart ;
453- const textW : DevicePixels = Math . max ( 0 , floatW - ( textX - floatX ) ) ;
454-
455- if ( textW > textMeasurement . minWidth ) {
456- const fittedText = textMeasurement . getFittedText ( text , textW ) ;
457- if ( fittedText ) {
458- fastFillStyle . set (
459- isHovered || isSelected
460- ? colorStyles . selectedTextColor
461- : '#000000'
462- ) ;
463- ctx . fillText ( fittedText , textX , intY + textDevicePixelsOffsetTop ) ;
464- }
443+ const isHovered =
444+ hoveredItem &&
445+ depth === hoveredItem . depth &&
446+ i === hoveredItem . stackTimingIndex ;
447+
448+ const colorStyles = mapCategoryColorNameToStackChartStyles (
449+ category . color
450+ ) ;
451+ // Draw the box.
452+ fastFillStyle . set (
453+ isHovered || isSelected
454+ ? colorStyles . selectedFillStyle
455+ : colorStyles . unselectedFillStyle
456+ ) ;
457+ ctx . fillRect (
458+ intX ,
459+ intY ,
460+ // Add on a bit of BORDER_OPACITY to the end of the width, to draw a partial
461+ // pixel. This will effectively draw a transparent version of the fill color
462+ // without having to change the fill color. At the time of this writing it
463+ // was the same performance cost as only providing integer values here.
464+ intW + BORDER_OPACITY ,
465+ intH
466+ ) ;
467+ lastDrawnPixelX =
468+ intX +
469+ intW +
470+ // The border on the right is 1 device pixel wide.
471+ 1 ;
472+
473+ // Draw the text label if it fits. Use the original float values here so that
474+ // the text doesn't snap around when moving. Only the boxes should snap.
475+ const textX : DevicePixels =
476+ // Constrain the x coordinate to the leftmost area.
477+ Math . max ( floatX , 0 ) + textDevicePixelsOffsetStart ;
478+ const textW : DevicePixels = Math . max ( 0 , floatW - ( textX - floatX ) ) ;
479+
480+ if ( textW > textMeasurement . minWidth ) {
481+ const fittedText = textMeasurement . getFittedText ( text , textW ) ;
482+ if ( fittedText ) {
483+ fastFillStyle . set (
484+ isHovered || isSelected
485+ ? colorStyles . selectedTextColor
486+ : '#000000'
487+ ) ;
488+ ctx . fillText ( fittedText , textX , intY + textDevicePixelsOffsetTop ) ;
465489 }
466490 }
467491 }
@@ -666,10 +690,10 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
666690
667691 if (
668692 this . _sameWidthsRangeLength === null ||
669- this . _sameWidthsIndexAtStart === null
693+ this . _sameWidthsIndexAtViewportStart === null
670694 ) {
671695 console . warn (
672- 'The local variables sameWidthsRangeLength or samewidthsIndexAtStart are null when they should be present.'
696+ 'The local variables sameWidthsRangeLength or sameWidthsIndexAtViewportStart are null when they should be present.'
673697 ) ;
674698 return null ;
675699 }
@@ -680,7 +704,7 @@ class StackChartCanvasImpl extends React.PureComponent<Props> {
680704 const xMinusMargin = x - marginLeft ;
681705 const hoveredBox =
682706 ( xMinusMargin / innerContainerWidth ) * this . _sameWidthsRangeLength +
683- this . _sameWidthsIndexAtStart ;
707+ this . _sameWidthsIndexAtViewportStart ;
684708
685709 for ( let i = 0 ; i < stackTiming . length ; i ++ ) {
686710 const start = stackTiming . sameWidthsStart [ i ] ;
0 commit comments