From 73571dc250908c06dccd6498a0db8b0c214e64cd Mon Sep 17 00:00:00 2001 From: Florian Rival Date: Mon, 23 Sep 2024 18:12:41 +0200 Subject: [PATCH] Add basic profiler to the scene editor --- .../FullSizeInstancesEditorWithScrollbars.js | 1 + .../BasicProfilingCounters.js | 150 ++++++++++++++++++ .../InstancesRenderer/LayerRenderer.js | 27 +++- .../InstancesRenderer/index.js | 28 ++++ newIDE/app/src/InstancesEditor/ProfilerBar.js | 72 +++++++++ newIDE/app/src/InstancesEditor/index.js | 12 ++ .../Preferences/PreferencesContext.js | 4 + .../Preferences/PreferencesDialog.js | 7 + .../Preferences/PreferencesProvider.js | 13 ++ .../SwipeableDrawerEditorsDisplay/index.js | 1 + 10 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 newIDE/app/src/InstancesEditor/InstancesRenderer/BasicProfilingCounters.js create mode 100644 newIDE/app/src/InstancesEditor/ProfilerBar.js diff --git a/newIDE/app/src/InstancesEditor/FullSizeInstancesEditorWithScrollbars.js b/newIDE/app/src/InstancesEditor/FullSizeInstancesEditorWithScrollbars.js index 6ec598ca5c1b..a78913ad8408 100644 --- a/newIDE/app/src/InstancesEditor/FullSizeInstancesEditorWithScrollbars.js +++ b/newIDE/app/src/InstancesEditor/FullSizeInstancesEditorWithScrollbars.js @@ -433,6 +433,7 @@ const FullSizeInstancesEditorWithScrollbars = (props: Props) => { } }} showObjectInstancesIn3D={values.use3DEditor} + showBasicProfilingCounters={values.showBasicProfilingCounters} {...otherProps} /> diff --git a/newIDE/app/src/InstancesEditor/InstancesRenderer/BasicProfilingCounters.js b/newIDE/app/src/InstancesEditor/InstancesRenderer/BasicProfilingCounters.js new file mode 100644 index 000000000000..913963101333 --- /dev/null +++ b/newIDE/app/src/InstancesEditor/InstancesRenderer/BasicProfilingCounters.js @@ -0,0 +1,150 @@ +// @flow + +type InstanceCounter = { + updateCount: number, + totalUpdateTime: number, +}; + +export type BasicProfilingCounters = { + instanceCounters: { [string]: InstanceCounter }, + totalInstancesUpdateCount: number, + totalInstancesUpdateTime: number, + totalPixiRenderingTime: number, + totalPixiUiRenderingTime: number, + totalThreeRenderingTime: number, +}; + +export const makeBasicProfilingCounters = (): BasicProfilingCounters => { + return { + instanceCounters: {}, + totalInstancesUpdateCount: 0, + totalInstancesUpdateTime: 0, + totalPixiRenderingTime: 0, + totalPixiUiRenderingTime: 0, + totalThreeRenderingTime: 0, + }; +}; + +export const resetBasicProfilingCounters = ( + basicProfilingCounters: BasicProfilingCounters +): BasicProfilingCounters => { + basicProfilingCounters.instanceCounters = {}; + basicProfilingCounters.totalInstancesUpdateCount = 0; + basicProfilingCounters.totalInstancesUpdateTime = 0; + basicProfilingCounters.totalPixiRenderingTime = 0; + basicProfilingCounters.totalPixiUiRenderingTime = 0; + basicProfilingCounters.totalThreeRenderingTime = 0; + return basicProfilingCounters; +}; + +export const increaseInstanceUpdate = ( + basicProfilingCounters: BasicProfilingCounters, + objectName: string, + updateDuration: number +) => { + let instanceCounter = basicProfilingCounters.instanceCounters[objectName]; + if (!instanceCounter) { + basicProfilingCounters.instanceCounters[objectName] = { + updateCount: 1, + totalUpdateTime: updateDuration, + }; + } else { + instanceCounter.updateCount++; + instanceCounter.totalUpdateTime += updateDuration; + } + basicProfilingCounters.totalInstancesUpdateCount++; + basicProfilingCounters.totalInstancesUpdateTime += updateDuration; +}; + +export const increasePixiRenderingTime = ( + basicProfilingCounters: BasicProfilingCounters, + pixiRenderingTime: number +) => { + basicProfilingCounters.totalPixiRenderingTime += pixiRenderingTime; +}; + +export const increasePixiUiRenderingTime = ( + basicProfilingCounters: BasicProfilingCounters, + pixiUiRenderingTime: number +) => { + basicProfilingCounters.totalPixiUiRenderingTime += pixiUiRenderingTime; +}; + +export const increaseThreeRenderingTime = ( + basicProfilingCounters: BasicProfilingCounters, + threeRenderingTime: number +) => { + basicProfilingCounters.totalThreeRenderingTime += threeRenderingTime; +}; + +export const mergeBasicProfilingCounters = ( + destination: BasicProfilingCounters, + source: BasicProfilingCounters +): BasicProfilingCounters => { + for (const objectName in source.instanceCounters) { + if (source.instanceCounters.hasOwnProperty(objectName)) { + const instanceCounter = source.instanceCounters[objectName]; + let destinationInstanceCounter = destination.instanceCounters[objectName]; + if (!destinationInstanceCounter) { + destinationInstanceCounter = destination.instanceCounters[ + objectName + ] = { + updateCount: 0, + totalUpdateTime: 0, + }; + } + destinationInstanceCounter.updateCount += instanceCounter.updateCount; + destinationInstanceCounter.totalUpdateTime += + instanceCounter.totalUpdateTime; + } + } + destination.totalInstancesUpdateCount += source.totalInstancesUpdateCount; + destination.totalInstancesUpdateTime += source.totalInstancesUpdateTime; + destination.totalPixiRenderingTime += source.totalPixiRenderingTime; + destination.totalPixiUiRenderingTime += source.totalPixiUiRenderingTime; + destination.totalThreeRenderingTime += source.totalThreeRenderingTime; + return destination; +}; + +export const getBasicProfilingCountersText = ( + basicProfilingCounters: BasicProfilingCounters +): string => { + const texts = []; + texts.push( + `Instances update count: ${ + basicProfilingCounters.totalInstancesUpdateCount + }` + ); + texts.push( + `Instances update time: ${basicProfilingCounters.totalInstancesUpdateTime.toFixed( + 2 + )}ms` + ); + texts.push( + `Pixi rendering time: ${basicProfilingCounters.totalPixiRenderingTime.toFixed( + 2 + )}ms` + ); + texts.push( + `Three rendering time: ${basicProfilingCounters.totalThreeRenderingTime.toFixed( + 2 + )}ms` + ); + texts.push( + `Pixi UI rendering time: ${basicProfilingCounters.totalPixiUiRenderingTime.toFixed( + 2 + )}ms` + ); + texts.push(' '); + for (const objectName in basicProfilingCounters.instanceCounters) { + const instanceCounters = + basicProfilingCounters.instanceCounters[objectName]; + texts.push( + `${objectName}: ${ + instanceCounters.updateCount + } updates, ${instanceCounters.totalUpdateTime.toFixed(2)}ms` + ); + } + + return texts.join('\n'); +}; diff --git a/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js b/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js index 2a98e1c6b092..0fa71d59f885 100644 --- a/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js +++ b/newIDE/app/src/InstancesEditor/InstancesRenderer/LayerRenderer.js @@ -16,6 +16,12 @@ import { type Polygon, } from '../../Utils/PolygonHelper'; import Rendered3DInstance from '../../ObjectsRendering/Renderers/Rendered3DInstance'; +import { + type BasicProfilingCounters, + increaseInstanceUpdate, + makeBasicProfilingCounters, + resetBasicProfilingCounters, +} from './BasicProfilingCounters'; const gd: libGDevelop = global.gd; export default class LayerRenderer { @@ -83,6 +89,8 @@ export default class LayerRenderer { _showObjectInstancesIn3D: boolean; + _basicProfilingCounters = makeBasicProfilingCounters(); + constructor({ project, globalObjectsContainer, @@ -186,7 +194,18 @@ export default class LayerRenderer { ? 'auto' : 'static'; } - if (isVisible) renderedInstance.update(); + if (isVisible) { + const objectName = instance.getObjectName(); + const time = performance.now(); + renderedInstance.update(); + const duration = performance.now() - time; + + increaseInstanceUpdate( + this._basicProfilingCounters, + objectName, + duration + ); + } if (renderedInstance instanceof Rendered3DInstance) { const threeObject = renderedInstance.getThreeObject(); @@ -555,6 +574,8 @@ export default class LayerRenderer { } render() { + resetBasicProfilingCounters(this._basicProfilingCounters); + this._computeViewBounds(); this.instances.iterateOverInstancesWithZOrdering( // $FlowFixMe - gd.castObject is not supporting typings. @@ -566,6 +587,10 @@ export default class LayerRenderer { this._destroyUnusedInstanceRenderers(); } + getBasicProfilingCounters(): BasicProfilingCounters { + return this._basicProfilingCounters; + } + /** * Create Three.js objects for 3D rendering of this layer. */ diff --git a/newIDE/app/src/InstancesEditor/InstancesRenderer/index.js b/newIDE/app/src/InstancesEditor/InstancesRenderer/index.js index 71435fdec6ae..a6f28f92b4fa 100644 --- a/newIDE/app/src/InstancesEditor/InstancesRenderer/index.js +++ b/newIDE/app/src/InstancesEditor/InstancesRenderer/index.js @@ -5,6 +5,15 @@ import * as PIXI from 'pixi.js-legacy'; import * as THREE from 'three'; import { rgbToHexNumber } from '../../Utils/ColorTransformer'; import Rectangle from '../../Utils/Rectangle'; +import { + type BasicProfilingCounters, + makeBasicProfilingCounters, + mergeBasicProfilingCounters, + resetBasicProfilingCounters, + increasePixiRenderingTime, + increaseThreeRenderingTime, + increasePixiUiRenderingTime, +} from './BasicProfilingCounters'; export type InstanceMeasurer = {| getInstanceAABB: (gdInitialInstance, Rectangle) => Rectangle, @@ -49,6 +58,8 @@ export default class InstancesRenderer { temporaryRectangle: Rectangle; instanceMeasurer: InstanceMeasurer; + _basicProfilingCounters = makeBasicProfilingCounters(); + constructor({ project, layersContainer, @@ -176,6 +187,10 @@ export default class InstancesRenderer { return this.instanceMeasurer; } + getBasicProfilingCounters(): BasicProfilingCounters { + return this._basicProfilingCounters; + } + render( pixiRenderer: PIXI.Renderer, threeRenderer: THREE.WebGLRenderer | null, @@ -183,6 +198,8 @@ export default class InstancesRenderer { uiPixiContainer: PIXI.Container, backgroundPixiContainer: PIXI.Container ) { + resetBasicProfilingCounters(this._basicProfilingCounters); + // Even if no rendering at all has been made already, setting up the Three.js/PixiJS renderers // might have changed some WebGL states already. Reset the state for the very first frame. // And, out of caution, keep doing it for every frame. @@ -244,6 +261,8 @@ export default class InstancesRenderer { layerRenderer.wasUsed = true; layerRenderer.getPixiContainer().zOrder = i; layerRenderer.render(); + mergeBasicProfilingCounters(this._basicProfilingCounters, layerRenderer.getBasicProfilingCounters()); + const layerContainer = layerRenderer.getPixiContainer(); viewPosition.applyTransformationToPixi(layerContainer); @@ -256,7 +275,9 @@ export default class InstancesRenderer { if (!threeRenderer) { // Render a layer with 2D rendering (PixiJS) only. + const time = performance.now(); pixiRenderer.render(layerContainer, { clear: false }); + increasePixiRenderingTime(this._basicProfilingCounters, performance.now() - time); } else { // Render a layer with 3D rendering, and possibly some 2D rendering too. const threeScene = layerRenderer.getThreeScene(); @@ -272,12 +293,14 @@ export default class InstancesRenderer { // Do the rendering of the PixiJS objects of the layer on the render texture. // Then, update the texture of the plane showing the PixiJS rendering, // so that the 2D rendering made by PixiJS can be shown in the 3D world. + const pixiStartTime = performance.now(); layerRenderer.renderOnPixiRenderTexture(pixiRenderer); layerRenderer.updateThreePlaneTextureFromPixiRenderTexture( // The renderers are needed to find the internal WebGL texture. threeRenderer, pixiRenderer ); + increasePixiRenderingTime(this._basicProfilingCounters, performance.now() - pixiStartTime); // It's important to reset the internal WebGL state of PixiJS, then Three.js // to ensure the 3D rendering is made properly by Three.js @@ -287,7 +310,10 @@ export default class InstancesRenderer { // Clear the depth as each layer is independent and display on top of the previous one, // even 3D objects. threeRenderer.clearDepth(); + + const threeStartTime = performance.now(); threeRenderer.render(threeScene, threeCamera); + increaseThreeRenderingTime(this._basicProfilingCounters, performance.now() - threeStartTime); } } } @@ -300,7 +326,9 @@ export default class InstancesRenderer { pixiRenderer.reset(); } + const time = performance.now(); pixiRenderer.render(uiPixiContainer); + increasePixiUiRenderingTime(this._basicProfilingCounters, performance.now() - time); if (threeRenderer) { // It's important to reset the internal WebGL state of PixiJS, then Three.js diff --git a/newIDE/app/src/InstancesEditor/ProfilerBar.js b/newIDE/app/src/InstancesEditor/ProfilerBar.js new file mode 100644 index 000000000000..a8fa206daff9 --- /dev/null +++ b/newIDE/app/src/InstancesEditor/ProfilerBar.js @@ -0,0 +1,72 @@ +// @flow +import * as PIXI from 'pixi.js-legacy'; +import { + getBasicProfilingCountersText, + type BasicProfilingCounters, +} from './InstancesRenderer/BasicProfilingCounters'; + +export default class ProfilerBar { + _profilerBarContainer: PIXI.Container; + _profilerBarBackground: PIXI.Graphics; + _profilerBarText: PIXI.Text; + + constructor() { + this._profilerBarContainer = new PIXI.Container(); + this._profilerBarContainer.alpha = 0.8; + this._profilerBarContainer.hitArea = new PIXI.Rectangle(0, 0, 0, 0); + this._profilerBarBackground = new PIXI.Graphics(); + this._profilerBarText = new PIXI.Text('', { + fontSize: 12, + fill: 0xffffff, + align: 'left', + }); + this._profilerBarContainer.addChild(this._profilerBarBackground); + this._profilerBarContainer.addChild(this._profilerBarText); + } + + getPixiObject(): PIXI.Container { + return this._profilerBarContainer; + } + + render({ + basicProfilingCounters, + display, + }: {| + basicProfilingCounters: BasicProfilingCounters, + display: boolean, + |}) { + if (!display) { + this._profilerBarContainer.visible = false; + return; + } + + this._profilerBarContainer.visible = true; + const textPadding = 5; + const profilerBarPadding = 15; + const borderRadius = 6; + const textXPosition = profilerBarPadding + textPadding; + const textYPosition = profilerBarPadding + textPadding; + + this._profilerBarText.text = getBasicProfilingCountersText( + basicProfilingCounters + ); + this._profilerBarText.position.x = textXPosition; + this._profilerBarText.position.y = textYPosition; + + const profilerBarXPosition = profilerBarPadding; + const profilerBarYPosition = profilerBarPadding; + const profilerBarWidth = this._profilerBarText.width + textPadding * 2; + const profilerBarHeight = this._profilerBarText.height + textPadding * 2; + + this._profilerBarBackground.clear(); + this._profilerBarBackground.beginFill(0x000000, 0.8); + this._profilerBarBackground.drawRoundedRect( + profilerBarXPosition, + profilerBarYPosition, + profilerBarWidth, + profilerBarHeight, + borderRadius + ); + this._profilerBarBackground.endFill(); + } +} diff --git a/newIDE/app/src/InstancesEditor/index.js b/newIDE/app/src/InstancesEditor/index.js index 6ee6d2f99e2b..f13bf9a2fb49 100644 --- a/newIDE/app/src/InstancesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/index.js @@ -23,6 +23,7 @@ import * as THREE from 'three'; import FpsLimiter from './FpsLimiter'; import { startPIXITicker, stopPIXITicker } from '../Utils/PIXITicker'; import StatusBar from './StatusBar'; +import ProfilerBar from './ProfilerBar'; import CanvasCursor from './CanvasCursor'; import InstancesAdder from './InstancesAdder'; import { makeDropTarget } from '../UI/DragAndDrop/DropTarget'; @@ -126,6 +127,7 @@ type Props = {| onMouseLeave?: MouseEvent => void, screenType: ScreenType, showObjectInstancesIn3D: boolean, + showBasicProfilingCounters: boolean, |}; type State = {| @@ -159,6 +161,7 @@ export default class InstancesEditor extends Component { windowBorder: WindowBorder; windowMask: WindowMask; statusBar: StatusBar; + profilerBar: ProfilerBar; uiPixiContainer: PIXI.Container; backgroundPixiContainer: PIXI.Container; backgroundArea: PIXI.Container; @@ -478,6 +481,9 @@ export default class InstancesEditor extends Component { if (this.background) { this.backgroundPixiContainer.removeChild(this.background.getPixiObject()); } + if (this.profilerBar) { + this.uiPixiContainer.removeChild(this.profilerBar.getPixiObject()); + } this.instancesRenderer = new InstancesRenderer({ project: props.project, @@ -568,6 +574,7 @@ export default class InstancesEditor extends Component { height: this.props.height, getLastCursorSceneCoordinates: this.getLastCursorSceneCoordinates, }); + this.profilerBar = new ProfilerBar(); this.uiPixiContainer.addChild(this.selectionRectangle.getPixiObject()); this.uiPixiContainer.addChild(this.instancesRenderer.getPixiContainer()); @@ -576,6 +583,7 @@ export default class InstancesEditor extends Component { this.uiPixiContainer.addChild(this.selectedInstances.getPixiContainer()); this.uiPixiContainer.addChild(this.highlightedInstance.getPixiObject()); this.uiPixiContainer.addChild(this.statusBar.getPixiObject()); + this.uiPixiContainer.addChild(this.profilerBar.getPixiObject()); this.uiPixiContainer.addChild(this.tileMapPaintingPreview.getPixiObject()); this.uiPixiContainer.addChild(this.clickInterceptor.getPixiObject()); @@ -1543,6 +1551,10 @@ export default class InstancesEditor extends Component { this.windowBorder.render(); this.windowMask.render(); this.statusBar.render(); + this.profilerBar.render({ + basicProfilingCounters: this.instancesRenderer.getBasicProfilingCounters(), + display: this.props.showBasicProfilingCounters + }); this.background.render(); this.instancesRenderer.render( diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js b/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js index f649813feae4..7c1da96aca2e 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesContext.js @@ -216,6 +216,7 @@ export type PreferencesValues = {| showDeprecatedInstructionWarning: boolean, openDiagnosticReportAutomatically: boolean, use3DEditor: boolean, + showBasicProfilingCounters: boolean, inAppTutorialsProgress: InAppTutorialProgressDatabase, newProjectsDefaultFolder: string, newProjectsDefaultStorageProviderName: string, @@ -299,6 +300,7 @@ export type Preferences = {| getShowDeprecatedInstructionWarning: () => boolean, setUse3DEditor: (enabled: boolean) => void, getUse3DEditor: () => boolean, + setShowBasicProfilingCounters: (enabled: boolean) => void, setNewProjectsDefaultStorageProviderName: (name: string) => void, saveTutorialProgress: ({| tutorialId: string, @@ -369,6 +371,7 @@ export const initialPreferences = { openDiagnosticReportAutomatically: true, showDeprecatedInstructionWarning: false, use3DEditor: isWebGLSupported(), + showBasicProfilingCounters: false, inAppTutorialsProgress: {}, newProjectsDefaultFolder: app ? findDefaultFolder(app) : '', newProjectsDefaultStorageProviderName: 'Cloud', @@ -436,6 +439,7 @@ export const initialPreferences = { getShowDeprecatedInstructionWarning: () => false, setUse3DEditor: (enabled: boolean) => {}, getUse3DEditor: () => false, + setShowBasicProfilingCounters: (enabled: boolean) => {}, saveTutorialProgress: () => {}, getTutorialProgress: () => {}, setNewProjectsDefaultFolder: () => {}, diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js b/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js index bc5bef886394..4a6570d38857 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesDialog.js @@ -75,6 +75,7 @@ const PreferencesDialog = ({ setOpenDiagnosticReportAutomatically, setShowDeprecatedInstructionWarning, setUse3DEditor, + setShowBasicProfilingCounters, setNewProjectsDefaultFolder, setUseShortcutToClosePreviewWindow, setWatchProjectFolderFilesForLocalProjects, @@ -448,6 +449,12 @@ const PreferencesDialog = ({ Show a warning on deprecated actions and conditions } /> + setShowBasicProfilingCounters(check)} + toggled={values.showBasicProfilingCounters} + labelPosition="right" + label={Display profiling information in scene editor} + /> setUse3DEditor(check)} toggled={values.use3DEditor} diff --git a/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js b/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js index d5cd065347da..4800a920276d 100644 --- a/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js +++ b/newIDE/app/src/MainFrame/Preferences/PreferencesProvider.js @@ -169,6 +169,7 @@ export default class PreferencesProvider extends React.Component { ), setUse3DEditor: this._setUse3DEditor.bind(this), getUse3DEditor: this._getUse3DEditor.bind(this), + setShowBasicProfilingCounters: this._setShowBasicProfilingCounters.bind(this), saveTutorialProgress: this._saveTutorialProgress.bind(this), getTutorialProgress: this._getTutorialProgress.bind(this), setNewProjectsDefaultFolder: this._setNewProjectsDefaultFolder.bind(this), @@ -511,6 +512,18 @@ export default class PreferencesProvider extends React.Component { return this.state.values.use3DEditor; } + _setShowBasicProfilingCounters(showBasicProfilingCounters: boolean) { + this.setState( + state => ({ + values: { + ...state.values, + showBasicProfilingCounters, + }, + }), + () => this._persistValuesToLocalStorage(this.state) + ); + } + _checkUpdates(forceDownload?: boolean) { // Checking for updates is only done on Electron. // Note: This could be abstracted away later if other updates mechanisms diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js index 5abf08db3cdf..93e5adccd544 100644 --- a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js @@ -268,6 +268,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< } pauseRendering={!props.isActive} showObjectInstancesIn3D={values.use3DEditor} + showBasicProfilingCounters={values.showBasicProfilingCounters} tileMapTileSelection={props.tileMapTileSelection} onSelectTileMapTile={props.onSelectTileMapTile} />