From 86c1453db91526006aea74847fefdcf623bfb441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20D=C3=ADaz?= Date: Wed, 7 Feb 2024 18:08:36 -0300 Subject: [PATCH] feat: Add Metrics Limits overlay (#890) --- .../components/Renderer/Metrics/Metrics.css | 133 +++++++++++++++ .../components/Renderer/Metrics/Metrics.tsx | 160 ++++++++++++++++++ .../src/components/Renderer/Metrics/index.ts | 2 + .../src/components/Renderer/Metrics/types.ts | 15 ++ .../src/components/Renderer/Metrics/utils.ts | 11 ++ .../src/components/Renderer/Renderer.css | 4 +- .../src/components/Renderer/Renderer.tsx | 2 + .../babylon/decentraland/layout-manager.ts | 4 +- .../@dcl/inspector/src/lib/utils/scene.ts | 1 + 9 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.css create mode 100644 packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.tsx create mode 100644 packages/@dcl/inspector/src/components/Renderer/Metrics/index.ts create mode 100644 packages/@dcl/inspector/src/components/Renderer/Metrics/types.ts create mode 100644 packages/@dcl/inspector/src/components/Renderer/Metrics/utils.ts diff --git a/packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.css b/packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.css new file mode 100644 index 000000000..826596228 --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.css @@ -0,0 +1,133 @@ +.Metrics { + --metrics-bottom: 8px; + --metrics-left: 8px; + --metrics-button-height: 30px; + --metrics-button-width: 30px; + + display: flex; + align-items: center; + position: absolute; + bottom: var(--metrics-bottom); + left: var(--metrics-left); + z-index: 1; +} + +.Metrics .Buttons { + display: flex; + flex-direction: row; + gap: 8px; +} + +.Metrics .Buttons .Button { + display: flex; + align-items: center; + justify-content: center; + height: var(--metrics-button-height); + width: var(--metrics-button-width); + padding: 5px; + border-radius: 4px; + background-color: transparent; +} + +.Metrics .Buttons .Button.Active svg { + color: var(--base-06); +} + +.Metrics .Buttons .Button svg { + color: var(--base-02); +} + +.Metrics > div.LimitExceeded { + display: flex; + align-items: center; + text-transform: uppercase; + cursor: default; +} + +.Metrics > .Overlay { + display: flex; + flex-direction: column; + width: 250px; + position: absolute; + overflow-y: auto; + left: 0; + bottom: calc(var(--metrics-bottom) + var(--metrics-button-height) + 8px); + background-color: var(--base-19); + padding: 13px 12px; + border-radius: 4px; + gap: 16px; +} + +.Metrics > .Overlay h2.Header { + display: flex; + font-size: 14px; + font-weight: 500; + line-height: 17px; + color: var(--base-01); + margin-bottom: 0; + gap: 4px; +} + +.Metrics > .Overlay .Item .Title, +.Metrics > .Overlay .Item .Description, +.Metrics > .Overlay .Item .Description .Key { + font-size: 11px; + font-weight: 500; + color: var(--base-01); + margin-bottom: 0; +} + +.Metrics > .Overlay .secondary { + color: var(--base-09); +} + +.Metrics > .Overlay .Items { + display: flex; + flex-direction: column; + gap: 8px; +} + +.Metrics > .Overlay .Items .Item { + display: flex; + gap: 4px; + padding: 8px 0; + border-bottom: 1px solid var(--base-18); +} + +.Metrics > .Overlay .Items .Item:last-of-type { + border-bottom: none; +} + +.Metrics > .Overlay .Item .Title { + display: flex; + flex: 1; + align-items: center; +} + +.Metrics > .Overlay .Item .Description { + display: flex; + flex: 1; + align-items: center; + gap: 4px; + line-height: 14px; +} + +.Metrics > .Overlay .Item .Description .Key { + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + height: 18px; + border-radius: 4px; + background-color: var(--base-13); +} + +.Metrics .Buttons .Button.LimitExceeded svg, +.Metrics > .Overlay .Item .Description.LimitExceeded, +.Metrics > .Overlay .Item .Description.LimitExceeded .secondary { + color: var(--error-dark); +} + +.Metrics .Buttons .Button.Active.LimitExceeded svg { + color: var(--error-main); +} diff --git a/packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.tsx b/packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.tsx new file mode 100644 index 000000000..a01a1057a --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Metrics/Metrics.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useEffect, useMemo } from 'react' +import cx from 'classnames' +import { IoGridOutline as SquaresGridIcon, IoAlertCircleOutline as AlertIcon } from 'react-icons/io5' +import { CrdtMessageType } from '@dcl/ecs' + +import { withSdk, WithSdkProps } from '../../../hoc/withSdk' +import { useChange } from '../../../hooks/sdk/useChange' +import { useOutsideClick } from '../../../hooks/useOutsideClick' +import type { Layout } from '../../../lib/utils/layout' +import { GROUND_MESH_PREFIX, PARCEL_SIZE } from '../../../lib/utils/scene' +import { Button } from '../../Button' +import { getSceneLimits } from './utils' +import type { Metrics } from './types' + +import './Metrics.css' + +const ICON_SIZE = 18 +const IGNORE_MATERIALS = ['layout_grid', 'grid', 'base-box', 'BackgroundSkyboxMaterial', 'BackgroundPlaneMaterial'] +const IGNORE_TEXTURES = [ + 'EffectLayerMainRTT', + 'HighlightLayerBlurRTT', + 'https://assets.babylonjs.com/environments/backgroundGround.png', + 'https://assets.babylonjs.com/environments/backgroundSkybox.dds', + 'https://assets.babylonjs.com/environments/environmentSpecular.env', + 'data:EnvironmentBRDFTexture0', + 'GlowLayerBlurRTT', + 'GlowLayerBlurRTT2' +] +const IGNORE_MESHES = ['BackgroundHelper', 'BackgroundPlane', 'BackgroundSkybox'] + +const Metrics = withSdk(({ sdk }) => { + const ROOT = sdk.engine.RootEntity + const [showMetrics, setShowMetrics] = React.useState(false) + const [metrics, setMetrics] = React.useState({ + triangles: 0, + entities: 0, + bodies: 0, + materials: 0, + textures: 0 + }) + const [sceneLayout, setSceneLayout] = React.useState({ + base: { x: 0, y: 0 }, + parcels: [] + }) + + const handleUpdateMetrics = useCallback(() => { + const meshes = sdk.scene.meshes.filter( + (mesh) => !(IGNORE_MESHES.includes(mesh.id) || mesh.id.startsWith(GROUND_MESH_PREFIX)) + ) + const triangles = meshes.reduce((acc, mesh) => acc + mesh.getTotalVertices(), 0) + const entities = (sdk.components.Nodes.getOrNull(ROOT)?.value ?? [ROOT]).length - 1 + const uniqueTextures = new Set( + sdk.scene.textures + .filter((texture) => !IGNORE_TEXTURES.includes(texture.name)) + .map((texture) => texture.getInternalTexture()!.uniqueId) + ) + const uniqueMaterials = new Set( + sdk.scene.materials.map((material) => material.id).filter((id) => !IGNORE_MATERIALS.includes(id)) + ) + setMetrics({ + triangles: triangles, + entities: entities, + bodies: meshes.length, + materials: uniqueMaterials.size, + textures: uniqueTextures.size + }) + }, [sdk]) + + const handleUpdateSceneLayout = useCallback(() => { + const scene = sdk.components.Scene.get(ROOT) + setSceneLayout({ ...(scene.layout as Layout) }) + }, [sdk, setSceneLayout]) + + useEffect(() => { + sdk.scene.onDataLoadedObservable.add(handleUpdateMetrics) + sdk.scene.onMeshRemovedObservable.add(handleUpdateMetrics) + handleUpdateSceneLayout() + + return () => { + sdk.scene.onDataLoadedObservable.removeCallback(handleUpdateMetrics) + sdk.scene.onMeshRemovedObservable.removeCallback(handleUpdateMetrics) + } + }, []) + + useChange( + ({ operation, component }) => { + if (operation === CrdtMessageType.PUT_COMPONENT && component?.componentId === sdk.components.Scene.componentId) { + handleUpdateSceneLayout() + } + }, + [handleUpdateSceneLayout] + ) + + const limits = useMemo(() => { + const parcels = sceneLayout.parcels.length + return getSceneLimits(parcels) + }, [sceneLayout]) + + const limitsExceeded = useMemo>(() => { + return Object.fromEntries( + Object.entries(metrics) + .map(([key, value]) => [key, value > limits[key as keyof Metrics]]) + .filter(([, value]) => value) + ) + }, [metrics, limits]) + + const handleToggleMetricsOverlay = useCallback( + (e: React.MouseEvent | MouseEvent) => { + e.preventDefault() + e.stopPropagation() + setShowMetrics((value) => !value) + }, + [showMetrics, setShowMetrics] + ) + + const overlayRef = useOutsideClick(handleToggleMetricsOverlay) + + return ( +
+
+ +
+ {Object.values(limitsExceeded).length > 0 && ( +
+ + Too many {Object.keys(limitsExceeded)[0].toUpperCase()} +
+ )} + {showMetrics && ( +
+

+ {sceneLayout.parcels.length} Parcels + + {sceneLayout.parcels.length * PARCEL_SIZE}m2 + +

+
+ {Object.entries(metrics).map(([key, value]) => ( +
+
{key.toUpperCase()}
+
+ {value} + {'/'} + {limits[key as keyof Metrics]} +
+
+ ))} +
+
+ )} +
+ ) +}) + +export default React.memo(Metrics) diff --git a/packages/@dcl/inspector/src/components/Renderer/Metrics/index.ts b/packages/@dcl/inspector/src/components/Renderer/Metrics/index.ts new file mode 100644 index 000000000..afb1f3d88 --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Metrics/index.ts @@ -0,0 +1,2 @@ +import Metrics from './Metrics' +export { Metrics } diff --git a/packages/@dcl/inspector/src/components/Renderer/Metrics/types.ts b/packages/@dcl/inspector/src/components/Renderer/Metrics/types.ts new file mode 100644 index 000000000..32e9e3a64 --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Metrics/types.ts @@ -0,0 +1,15 @@ +export interface Metrics { + triangles: number + entities: number + bodies: number + materials: number + textures: number +} + +export enum Limits { + triangles = 10000, + entities = 200, + bodies = 300, + materials = 20, + textures = 10 +} diff --git a/packages/@dcl/inspector/src/components/Renderer/Metrics/utils.ts b/packages/@dcl/inspector/src/components/Renderer/Metrics/utils.ts new file mode 100644 index 000000000..b06532202 --- /dev/null +++ b/packages/@dcl/inspector/src/components/Renderer/Metrics/utils.ts @@ -0,0 +1,11 @@ +import { Limits, type Metrics } from './types' + +export function getSceneLimits(parcels: number): Metrics { + return { + triangles: parcels * Limits.triangles, + entities: parcels * Limits.entities, + bodies: parcels * Limits.bodies, + materials: Math.floor(Math.log2(parcels + 1) * Limits.materials), + textures: Math.floor(Math.log2(parcels + 1) * Limits.textures) + } +} diff --git a/packages/@dcl/inspector/src/components/Renderer/Renderer.css b/packages/@dcl/inspector/src/components/Renderer/Renderer.css index 779f3e019..e38dbacdd 100644 --- a/packages/@dcl/inspector/src/components/Renderer/Renderer.css +++ b/packages/@dcl/inspector/src/components/Renderer/Renderer.css @@ -3,11 +3,11 @@ position: relative; } -.Renderer>div:first-child { +.Renderer > div:first-child { display: flex; } -.Renderer>canvas { +.Renderer > canvas { width: 100%; height: 100%; touch-action: none; diff --git a/packages/@dcl/inspector/src/components/Renderer/Renderer.tsx b/packages/@dcl/inspector/src/components/Renderer/Renderer.tsx index b5a994e9c..3a746d230 100644 --- a/packages/@dcl/inspector/src/components/Renderer/Renderer.tsx +++ b/packages/@dcl/inspector/src/components/Renderer/Renderer.tsx @@ -42,6 +42,7 @@ import { analytics, Event } from '../../lib/logic/analytics' import { Warnings } from '../Warnings' import { CameraSpeed } from './CameraSpeed' import { Shortcuts } from './Shortcuts' +import { Metrics } from './Metrics' import './Renderer.css' @@ -261,6 +262,7 @@ const Renderer: React.FC = () => { {isLoading && } + diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/layout-manager.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/layout-manager.ts index ddac85310..a22c31dd4 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/layout-manager.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/layout-manager.ts @@ -15,7 +15,7 @@ import { import { memoize } from '../../logic/once' import { Layout } from '../../utils/layout' import { GridMaterial } from '@babylonjs/materials' -import { PARCEL_SIZE } from '../../utils/scene' +import { PARCEL_SIZE, GROUND_MESH_PREFIX } from '../../utils/scene' function disableGizmo(gizmo: IAxisDragGizmo) { gizmo.dragBehavior.detach() @@ -104,7 +104,7 @@ export const getLayoutManager = memoize((scene: Scene) => { const x = parcel.x - base.x const y = parcel.y - base.y - const plane = MeshBuilder.CreatePlane(`ground_plane_${x}_${y}`, { size: PARCEL_SIZE }, scene) + const plane = MeshBuilder.CreatePlane(`${GROUND_MESH_PREFIX}_${x}_${y}`, { size: PARCEL_SIZE }, scene) plane.parent = layoutNode plane.position.x = x * PARCEL_SIZE plane.position.z = y * PARCEL_SIZE diff --git a/packages/@dcl/inspector/src/lib/utils/scene.ts b/packages/@dcl/inspector/src/lib/utils/scene.ts index 955642b2f..f929511c7 100644 --- a/packages/@dcl/inspector/src/lib/utils/scene.ts +++ b/packages/@dcl/inspector/src/lib/utils/scene.ts @@ -1 +1,2 @@ export const PARCEL_SIZE = 16 +export const GROUND_MESH_PREFIX = 'ground_plane'