Skip to content

Inspector v2: Add lightweight runtime "instrumentation" helpers and a usage example #16748

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 13, 2025

Conversation

ryantrem
Copy link
Member

@ryantrem ryantrem commented Jun 13, 2025

One of the problems we currently have in tooling (and especially in inspector v1) is having the UI react to scene state changes. Ideally we'd have observables for every mutable state of a scene, but the runtime overhead would be too high to make this practical. We currently work around this in one of a few ways:

  1. In some of the tools, we use a global observable, where any part of the tool UI that mutates any scene state is supposed to fire the observable. This way all the different parts of the tool UI can communicate state changes that they initiate.
  2. When state changes don't come from the tools (e.g. Playground, or really any app that uses Inspector), we just poll for state changes (e.g. continuously re-render react components every ~500ms for example to detect changes).
  3. Provide a refresh button, where the user must click the button to see changes.

This PR introduces a new idea, which I refer to as "lightweight runtime instrumentation." The idea is to temporarily hook function calls and property setters to produce a transient observable. This would not scale if we tried to hook every property on every object of the scene simultaneously, but in reality only a very small part of the scene state is bound to the inspector UI at any time, e.g.:

  1. The properties of a single object (in the properties pane).
  2. The names of as many nodes as fit into scene explorer (since it virtualizes, so there are not that many live react components for tree view nodes bound to scene entities).
    When a different entity is bound to the properties pane, or the scene explorer is scrolled, the temporary hooks are removed and the entity objects are restored to their original state.

With this in mind, hooking functions/properties to observe scene state changes specifically for tooling should be quite low overhead, so this PR introduces the idea and has one example of the usage. There are helper functions for "intercepting" function calls and property setters, and then a React hook that translates this to React state, e.g.:
const computeBonesUsingShadersObservable = useInterceptObservable("property", mesh, "computeBonesUsingShaders")
This observable (and the temporary instrumentation of the computeBonesUsingShaders property) is reverted as soon as the consuming react component is unmounted.

Take a look in this order:

  1. The useObservableState in meshGeneralProperties.tsx - this is just showing an existing concept where given an observable, we can produce React state that the component can use to re-render.
  2. The useInterceptObservable used along with useObservableState in meshAdvancedProperties.tsx - this shows using the new useInterceptObservable to create a temporary observable that lets us immediately respond to state changes of a property that doesn't actually have an observable.
  3. The useObservableState implementation in instrumentationHooks.ts.
  4. The InterceptFunction helper function in functionInstrumentation.ts.
  5. The InterceptProperty helper function in propertyInstrumentation.ts.

Open to feedback on this idea. So far it seems like it works quite well.

@bjsplat
Copy link
Collaborator

bjsplat commented Jun 13, 2025

Please make sure to label your PR with "bug", "new feature" or "breaking change" label(s).
To prevent this PR from going to the changelog marked it with the "skip changelog" label.

@bjsplat
Copy link
Collaborator

bjsplat commented Jun 13, 2025

@bjsplat
Copy link
Collaborator

bjsplat commented Jun 13, 2025

@bjsplat
Copy link
Collaborator

bjsplat commented Jun 13, 2025

@ryantrem ryantrem marked this pull request as ready for review June 13, 2025 04:53
Copy link
Member

@sebavan sebavan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but lets be sure we are not hooking everything all the time.

I guess It should for instance never be on when looking at the scene stats.

@ryantrem
Copy link
Member Author

LGTM but lets be sure we are not hooking everything all the time.

I guess It should for instance never be on when looking at the scene stats.

Yes that's exactly how I was testing actually. :)
Of course it is possible we would use this in scene stats if it made sense, but I think the main thing we're saying here is that the only things that should be hooked at any given moment are what objects the UI is currently bound to (and ideally only the visible UI). In this case that is exactly how it works, because when you switch tabs, the previous tab's React component is unmounted.

@ryantrem ryantrem merged commit 338237b into BabylonJS:master Jun 13, 2025
18 checks passed
VicenteCartas pushed a commit to VicenteCartas/Babylon.js that referenced this pull request Aug 7, 2025
… usage example (BabylonJS#16748)

One of the problems we currently have in tooling (and especially in
inspector v1) is having the UI react to scene state changes. Ideally
we'd have observables for every mutable state of a scene, but the
runtime overhead would be too high to make this practical. We currently
work around this in one of a few ways:

1. In some of the tools, we use a global observable, where any part of
the tool UI that mutates any scene state is supposed to fire the
observable. This way all the different parts of the tool UI can
communicate state changes that they initiate.
2. When state changes don't come from the tools (e.g. Playground, or
really any app that uses Inspector), we just poll for state changes
(e.g. continuously re-render react components every ~500ms for example
to detect changes).
3. Provide a refresh button, where the user must click the button to see
changes.

This PR introduces a new idea, which I refer to as "lightweight runtime
instrumentation." The idea is to temporarily hook function calls and
property setters to produce a transient observable. This would not scale
if we tried to hook every property on every object of the scene
simultaneously, but in reality only a very small part of the scene state
is bound to the inspector UI at any time, e.g.:
1. The properties of a single object (in the properties pane).
2. The names of as many nodes as fit into scene explorer (since it
virtualizes, so there are not that many live react components for tree
view nodes bound to scene entities).
When a different entity is bound to the properties pane, or the scene
explorer is scrolled, the temporary hooks are removed and the entity
objects are restored to their original state.

With this in mind, hooking functions/properties to observe scene state
changes specifically for tooling should be quite low overhead, so this
PR introduces the idea and has one example of the usage. There are
helper functions for "intercepting" function calls and property setters,
and then a React hook that translates this to React state, e.g.:
`const computeBonesUsingShadersObservable =
useInterceptObservable("property", mesh, "computeBonesUsingShaders")`
This observable (and the temporary instrumentation of the
`computeBonesUsingShaders` property) is reverted as soon as the
consuming react component is unmounted.

Take a look in this order:

1. The `useObservableState` in
[meshGeneralProperties.tsx](https://github.com/BabylonJS/Babylon.js/pull/16748/files#diff-dca74d1795e7e366439f24e66a2a26fe0b14ef5f3fc4ceb3b05585033b781955R10)
- this is just showing an existing concept where given an observable, we
can produce React state that the component can use to re-render.
2. The `useInterceptObservable` used along with `useObservableState` in
[meshAdvancedProperties.tsx](https://github.com/BabylonJS/Babylon.js/pull/16748/files#diff-1fc176dd99343ae0ff259bf520c6504813ab7c02fbbb06292280310d7a2e588dR11)
- this shows using the new `useInterceptObservable` to create a
temporary observable that lets us immediately respond to state changes
of a property that doesn't actually have an observable.
4. The `useObservableState` implementation in
[instrumentationHooks.ts](https://github.com/BabylonJS/Babylon.js/pull/16748/files#diff-92b79d9fbaaf8f9eef52315919e1ae5640553f1262ca2c43618f091ebb29de4dR19).
5. The `InterceptFunction` helper function in
[functionInstrumentation.ts](https://github.com/BabylonJS/Babylon.js/pull/16748/files#diff-0124bc224339ce336c700bf1128d94268f9abcc1ad92cebeb171e825e250d07cR20).
6. The `InterceptProperty` helper function in
[propertyInstrumentation.ts](https://github.com/BabylonJS/Babylon.js/pull/16748/files#diff-ec2332f5239be4ddf8eab5cb2faa5bb1cf36da855b214cbc44a25f43981a6496R20).

Open to feedback on this idea. So far it seems like it works quite well.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants