Repository: facebook/react
Pull request: Scheduling profiler: Canvas views clip by default

Root `View` now auto-clips canvas before calling subview `draw()` method to prevent possible overflows in views that forget to clip explicitly.

### Before this fix

https://user-images.githubusercontent.com/29597/129596698-218384ff-abd2-4756-bf4d-6625af029bae.mp4

### After this fix

https://user-images.githubusercontent.com/29597/129596701-3eec639d-a90b-484c-a0ef-fa0c26a25cfb.mp4

I removed the `_needsDisplay` and `_subviewsNeedDisplay` checks to force all views to always draw, and then used the `performance` API to measure the cost of drawing all Canvas views before and after this change to make sure it didn't negatively impact performance. After a couple thousand renders, we're looking at 4-5ms duration (for a full redraw) both before and after this change. So it seems performance neutral.

Changed files:
filename: packages/react-devtools-scheduling-profiler/src/content-views/SnapshotsView.js
patch: @@ -118,6 +118,7 @@ export class SnapshotsView extends View {
     const visibleArea = this.visibleArea;
 
     // Prevent snapshot from visibly overflowing its container when clipped.
+    // View clips by default, but since this view may draw async (on Image load) we re-clip.
     const shouldClip = !rectEqualToRect(imageRect, visibleArea);
     if (shouldClip) {
       const clippedRect = intersectionOfRects(imageRect, visibleArea);

filename: packages/react-devtools-scheduling-profiler/src/content-views/SuspenseEventsView.js
patch: @@ -168,26 +168,13 @@ export class SuspenseEventsView extends View {
         return; // Not in view
       }
 
-      const drawableRect = intersectionOfRects(suspenseRect, rect);
-
-      // Clip diamonds so they don't overflow if the view has been resized (smaller).
-      const region = new Path2D();
-      region.rect(
-        drawableRect.origin.x,
-        drawableRect.origin.y,
-        drawableRect.size.width,
-        drawableRect.size.height,
-      );
-      context.save();
-      context.clip(region);
       context.beginPath();
       context.fillStyle = fillStyle;
       context.moveTo(xStart, y - halfSize);
       context.lineTo(xStart + halfSize, y);
       context.lineTo(xStart, y + halfSize);
       context.lineTo(xStart - halfSize, y);
       context.fill();
-      context.restore();
     } else {
       const xStop = timestampToPosition(
         timestamp + duration,

filename: packages/react-devtools-scheduling-profiler/src/view-base/View.js
patch: @@ -199,7 +199,24 @@ export class View {
       this.layoutSubviews();
       if (this._needsDisplay) this._needsDisplay = false;
       if (this._subviewsNeedDisplay) this._subviewsNeedDisplay = false;
+
+      // Clip anything drawn by the view to prevent it from overflowing its visible area.
+      const visibleArea = this.visibleArea;
+      const region = new Path2D();
+      region.rect(
+        visibleArea.origin.x,
+        visibleArea.origin.y,
+        visibleArea.size.width,
+        visibleArea.size.height,
+      );
+      context.save();
+      context.clip(region);
+      context.beginPath();
+
       this.draw(context, viewRefs);
+
+      // Stop clipping
+      context.restore();
     }
   }