You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Get hold of a larger profile dataset (kind of done, I've generated a pretty large profile by clicking around the concurrent demo app. We'd ideally want to get hold of a profile from production though)
Figure out what the slowest points are
Design and implement a fix
... Further scope this task
Main user actions that cause renders
Cursor traversal over empty (i.e. no React data or flamegraph node) area
Investigate this; are we repainting the entire canvas?
Cursor hover over React data or flamegraph node
Currently, we're repainting the entire canvas, when the only thing that changes is the highlighted data/node.
Implement a rudimentary layout system that allows us to determine an area of the canvas to redraw, instead of redrawing the entire canvas. This will likely not be enough for larger profiles as the cost of rendering an entire section is pretty high. However, this will allow us to formalize and expand the scope of the rudimentary section stacking that we have now.
Scrolling/zooming/resizing/reloading.
A rerender of the entire canvas is necessary, unless there's a way to translate the canvas buffer.
Slow renders will mean jittery scrolling.
Potential optimizations:
Unclear, but all will likely be in the individual render functions. We should look into how Chrome's performance tab is so smooth. (Update: Chrome isn't very smooth when there are many flamegraph events either) I don't think offscreen canvas will help here as almost all the computation is related to rendering the canvas.
Don't render React events when they're not visible.
Wild idea: investigate using WebGL instead of 2D canvas.
Implement rudimentary layout system similar to iOS's UIViews/CALayers. This is intended to allow us to determine an area of the canvas to redraw, instead of redrawing the entire canvas.
Requirements:
Basics:
Support bounding boxes: to prevent redrawing of the entire canvas.
Visibility detection: to provide a reusable way to avoid drawing of an invisible view.
Hit testing and event handling: so that we can get the hovered event without recomputing the canvas's layout, which the existing getHoveredEvent current does.
Subviews: to represent the logical sections. This will also let us support manual size overrides more smoothly.
Possible extensions:
Horizontal layouts, which will allow us to use the view system to lay out the React data/flamechart nodes as well.
"Scrolling" and "zooming"; translating some views' contents by a certain offset. We could implement UIScrollView if we have horizontal layouts but that sounds a bit overkill right now.
If we implement scroll views and horizontal layouts, we can completely separate scrolling/zooming from the layout code.
Pros:
Formalizes the stacked logic that we have now.
Deduplicates the vertical drawing skipping code, allowing the rendering code to focus on drawing the data in their views.
Many potential extensions.
Allows the mouse hover code to depend on the drawing/layout code, instead of recomputing the locations of everything.
We can split this into a reusable package and open source it.
Cons:
May be overkill for our use case.
Does not improve the speed of a whole-screen render.
Does not reduce the amount of rendering required for scrolling/zooming operations. We already have some optimizations that prevents us from drawing most offscreen things; our view system will just make this optimization reusable and transparent.
Questions:
Does something like that already exist? We may be able to use that instead of rolling our own.
Can we use the browser's layout engine instead? We basically require position: sticky and scrolling, so it'll be great if we could just use the browser to do the actual layouts for us, possibly in invisible divs.
Does clipping mean that items outside the clipping rectangle are not drawn?
Things tried:
Built a POC with ReactART.
Used this fork (Run requestAnimationFrame for no jank if a browser supports it sebmarkbage/art#24) that uses requestAnimationFrame instead of the existing setTimeout, drastically reducing the delay between React's commit phase and the commits being rendered onscreen (unlike in React DOM, ReactART's commit phase does not draw directly to the canvas; the Art library does it after or schedules another rendering task for that).
From inspecting the performance profiles, I found that ReactART's Rectangle was very slow as it drew rects using lines instead of calling the canvas context's rect method. As we are drawing a large number of rectangles, this was a significant bottleneck. I added a way to call the canvas context's rect to Art and ReactART:
Forked the fork of Art and implemented a Path.rect function that calls the canvas context's rect function
Changed ReactART's Rectangle class to call Path.rect if the radii are 0.
Use a for loop in our most common loop instead of .map.
Use React.memo on our components, especially FlamechartNode, to reduce unnecessary work during the reconciliation phase.
Resolution: Likely too slow to be suitable.
The API is very nice, and the hit testing and event handling are really convenient.
However, both React and ReactART are too slow, as we have too many items to render. This screenshot below shows a mouseover over a flamechart node in a big-ish profile with a total of 121813 nodes generated by clicking around my toy concurrent demo app. As there are too many React elements even in this toy profile, the reconciler takes a long time to do its work, even after React.memo has skipped the rendering of most elements. Additionally, when more nodes are visible, Art's canvas rendering code also takes a long time, seemingly due to its setTransform calls.
WebGL
Investigated Pixi.js and Two.js, but both are not performant enough to handle tens of thousands of rects every frame.
Optimize hot loop code
Replacing the division operation in this line with a constant reduced the runtime of renderFlamegraph by a surprising amount (eyeballed the flamegraph: it looks like that reduced the runtime by >60%). Looks like some work can be done here to optimize all the hot loops:
Tried using Prepack to lift up some of the math for us, but:
Could not run it on the compiled bundle as Prepack was complaining that document was unknown.
I could run it on the renderCanvas.js file, but it does not inline the constants file so it did not optimize those calculations. I did not look into this further as I'm not fully expecting Prepack to work completely and it'll take too much time.
Tried using Google Closure Compiler, but as expected there was no performance gain as it did not execute the division.
Steps
Main user actions that cause renders
Cursor traversal over empty (i.e. no React data or flamegraph node) area
Cursor hover over React data or flamegraph node
Currently, we're repainting the entire canvas, when the only thing that changes is the highlighted data/node.
Potential optimizations
Use a second canvas, layered on top of the existing one, that only displays hovered data/nodes. Recommended by https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas#Use_multiple_layered_canvases_for_complex_scenes. We need to see if there'll be any syncing issues if the canvas is scrolled when an item is hovered over.
Implement a rudimentary layout system that allows us to determine an area of the canvas to redraw, instead of redrawing the entire canvas. This will likely not be enough for larger profiles as the cost of rendering an entire section is pretty high. However, this will allow us to formalize and expand the scope of the rudimentary section stacking that we have now.
Scrolling/zooming/resizing/reloading.
A rerender of the entire canvas is necessary, unless there's a way to translate the canvas buffer.
Slow renders will mean jittery scrolling.
Potential optimizations:
Unclear, but all will likely be in the individual render functions. We should look into how Chrome's performance tab is so smooth. (Update: Chrome isn't very smooth when there are many flamegraph events either) I don't think offscreen canvas will help here as almost all the computation is related to rendering the canvas.
Don't render React events when they're not visible.
Wild idea: investigate using WebGL instead of 2D canvas.
Summary of options
UIViews
Update: Implemented in #80.
Implement rudimentary layout system similar to iOS's UIViews/CALayers. This is intended to allow us to determine an area of the canvas to redraw, instead of redrawing the entire canvas.
Requirements:
getHoveredEventcurrent does.UIScrollViewif we have horizontal layouts but that sounds a bit overkill right now.Pros:
Cons:
Questions:
position: stickyand scrolling, so it'll be great if we could just use the browser to do the actual layouts for us, possibly in invisible divs.ReactART
A canvas renderer that's in the React repo.
Pros:
Cons:
Questions:
Things tried:
requestAnimationFrameinstead of the existingsetTimeout, drastically reducing the delay between React's commit phase and the commits being rendered onscreen (unlike in React DOM, ReactART's commit phase does not draw directly to the canvas; the Art library does it after or schedules another rendering task for that).Rectanglewas very slow as it drew rects using lines instead of calling the canvas context'srectmethod. As we are drawing a large number of rectangles, this was a significant bottleneck. I added a way to call the canvas context'srectto Art and ReactART:Path.rectfunction that calls the canvas context'srectfunctionRectangleclass to callPath.rectif the radii are 0..map.React.memoon our components, especiallyFlamechartNode, to reduce unnecessary work during the reconciliation phase.Resolution: Likely too slow to be suitable.
The API is very nice, and the hit testing and event handling are really convenient.
However, both React and ReactART are too slow, as we have too many items to render. This screenshot below shows a mouseover over a flamechart node in a big-ish profile with a total of 121813 nodes generated by clicking around my toy concurrent demo app. As there are too many React elements even in this toy profile, the reconciler takes a long time to do its work, even after
React.memohas skipped the rendering of most elements. Additionally, when more nodes are visible, Art's canvas rendering code also takes a long time, seemingly due to itssetTransformcalls.WebGL
Investigated Pixi.js and Two.js, but both are not performant enough to handle tens of thousands of rects every frame.
Optimize hot loop code
Replacing the division operation in this line with a constant reduced the runtime of
renderFlamegraphby a surprising amount (eyeballed the flamegraph: it looks like that reduced the runtime by >60%). Looks like some work can be done here to optimize all the hot loops: