Doubts about 'add_observer'

There was a topic about this here:


but for me it’s still unclear.

I need to observe a lot of entities (face loops) which in case of huge models may be even thousands of entities.
I have one global observer instance which is responsible for processing of all events.

And now, if I use this snippet:

unless @observer = nil
object.remove_observer @observer
object.add_observer @observer
end

First initialization (entity doesn’t contains my observer and “remove_observer” returns false) everything work quite fast - adding observers takes less than a second in case of huge model.

But if I try to reinitialize observers (some of entities may have had an observer before) remove_observer take a lot of time - even 20 seconds in case of huge model.

I tried to ‘add_observer’ multiple times (same observer instance) on entity and I saw that I get only one event per entity (it doesn’t matter how many times I added the observer instance).

Even more, multiple adding of the same observer instance, always gives me ‘true’ as a result of ‘add_observer’ but only first execution of ‘remove_observer’ returns true.

object.add_observer @observer #returns true
object.add_observer @observer #returns true

object.remove_observer @observer #returns true
object.remove_observer @observer #returns false

1). Could anyone explain how properly add/remove observers?
(SU Team: Could you share a bit of knowledge - how it works under the hood)

2). Is it safe to multiple adding of same observer instance?
(SU Team: the documentation in RubyAPI is very poor - maybe a few more sentences should appear under “add_observer” method)

3). Why removing observer instance taking so much time?

These questions were answered for me, in the past, by @bugra (Bugra Barin).
It would be best if he or @tt_su (Thomas Thomassen) weighs in on them.

We’re been talking about adding separate sections to the Ruby API documentation to explain key concepts that doesn’t otherwise fit into a class or method description. I’ll add a note about observers being one of these categories.

I need a little bit of time to compose a proper answer to this.

Can you produce a complete reproducible example please? This isn’t something I’ve seen myself - nor have heard reported. (Any chance you attaching EntityObservers to every single entity?)

1 Like

First of all, let me just provide some history of the Observer in SketchUp Ruby API. They were added in version 6 and were coupled very closely to the internal notification system SketchUp use. This wasn’t entirely ideal for API usage as they don’t always trigger consistently as you’d expect as an API user.

And they were very fragile, prone to trigger crashes. Back then, when I was simply a user and not employee, I struggled a lot with them. Even made this chart: http://www.thomthom.net/software/sketchup/observers/

In SU2016 we made some effort into further improve their stability, in terms of crashes etc. But changing the API couldn’t be done as it’d break too many extensions.

Over the years of writing extensions my mantra for observers has been, use only as few as you really need. And then try to use less.

When you describe, I’d like to hear the higher level description of what you are doing and why. I find that often when it comes to observer issues it’s better to talk about the higher lever logic and use-cases rather than focusing on the small techincal details. At least first - as often an alternate pattern can be better employed. Particularity when you are talking about monitoring thousands of entities - individually.

It’s also help to know exactly what observers we are talking about here as well.

Meanwhile, lemme try to shed some light on the other details you asked about:

I really want to see some figures here, how many faces/edges do the model have. The notion of “huge” is too subjective.

Yea, the return value of add_observer isn’t clear from the docs. I had to dive into the code and it really reflects whether or not the method was successful. When you add an observer to an entity that it’s already observer that is considered a success.

remove_observer returns false when the observer wasn’t attached to the entity.

Adding the same observer instance for an entity will not give duplicate signals, if that’s what you mean by safe.

class MyEntityObserver < Sketchup::EntityObserver
  def onChangeEntity(entity)
    puts "#{self.class}.onChangeEntity: #{entity}"
  end
end

# Attach the observer. (Assumes there is an entity in the model.)
Sketchup.active_model.entities[0].add_observer(MyEntityObserver.new)
Sketchup.active_model.entities[0].add_observer(MyEntityObserver.new)



class MyEntityObserver2 < Sketchup::EntityObserver
  def onChangeEntity(entity)
    puts "#{self.class}.onChangeEntity: #{entity}"
  end
end

# Attach the observer. (Assumes there is an entity in the model.)
observer = MyEntityObserver2.new
Sketchup.active_model.entities[0].add_observer(observer)
Sketchup.active_model.entities[0].add_observer(observer)

When I modify the entity the results are:

MyEntityObserver.onChangeEntity: #<Sketchup::Edge:0x00021936e0dd68>
MyEntityObserver.onChangeEntity: #<Sketchup::Edge:0x00021936e0dd68>
MyEntityObserver2.onChangeEntity: #<Sketchup::Edge:0x00021936e0dd68>

If you mean if it’s safe to use the same observer to multiple entities, then that’s also ok.

1 Like

The problem is how to properly handle push/pull and similar operations.

Using EntitiesObserver on entities level, gives us event only for pushed/pulled face. Other faces, which has been affected by push / pull do not cause an event. Now we can add EntityObserver to every Face.loop to get event (I think that I saw this solution somewhere on forum) or read all faces (there is no timestamp or smth to be able to validate if face was modified or not) on entities level (or traverse all faces connected to affected face) to check if something happened and we need react.

It’s clear now.

Thank you - I had the same conclusions after my tests, but I wanted to be sure.

Maybe a tool observer and possibly a selection observer could be of use?

I’ve also been in situations when I wanted to listen to a large amount of entities, in my case to intercept the user trying to open a group generated by my plugin and recommend them to edit it using the plugin instead of opening up the group and manually draw to it. In the end I used a model observer that listened to a change in the active path rather than listening to each and every single instance made by the plugin.

I think ThomThom really hits the nail on the head with this.

1 Like

I have avoided using Observers for 3 reasons:

  1. The documentation is so thin as to invite multiple (mis)interpretations. No examples. How much trial and error testing am I willing to do?
  2. The main observer I might want to use is EntityObserver where I want to change an entity in the observer. But it comes with the warning: “The methods of this observer fire in such a way that making changes to the model while inside of them is dangerous.” Perhaps this is because such a change would trigger an observer infinite loop(?). If so, maybe this could be fixed and would not break existing extensions because extensions that crash would not have been published.
  3. There seems to be no way to observe what property of the entity has changed: Material? Transform? Vertices?

So I can’t imagine what Rapit wants to do with EntityObserver that would NOT involve changes to the model?

1 Like

Just for reading the geometry to update my database in real time.

It can be even worse - if someone will select/unselect all faces on the scene I need to add/remove observer on each selecting action - now I do it only once at the beginning.

This sounds similar to what render engines do - those that have a separate render window that updates as you modify the model.

As far as I know none of them have gone to the extent you are - with attaching an observer to each loop. It sounds to me like a major challenge to manage that - and the sheer number of observer needed would not be surprising to me to add a performance hit.

You mentioned one scenario was push-pull, is it EntitiesObserver that isn’t giving enough information here? I’d really like to explore this further and see if we can steer you away from using thousands of EntityObservers.

What about attaching an EntitiesObserver when the suer opens a new drawing context to listen to all changes in it. Then the observer can be removed when the user leaves that drawing context. A ModelObserver can be used to attach and remove the EntitiesObserver as needed.

Lets look at EntitiesObserver and PushPull:

class MyEntitiesObserver < Sketchup::EntitiesObserver
  def onElementAdded(entities, entity)
    puts "onElementAdded: #{entity}"
  end
  def onElementModified(entities, entity)
    puts "onElementModified: #{entity}"
  end
  def onElementRemoved(entities, entity_id)
    puts "onElementRemoved: #{entity_id}"
  end
end

observer = MyEntitiesObserver.new
Sketchup.active_model.entities.add_observer(observer)

When I push-pulled a face from a flat surface - making an extrusion:
image

onElementAdded: #<Sketchup::Edge:0x0002667b2e8ab8>
onElementAdded: #<Sketchup::Edge:0x0002667b2e88d8>
onElementAdded: #<Sketchup::Edge:0x0002667b2e8770>
onElementAdded: #<Sketchup::Edge:0x0002667b2e85b8>
onElementAdded: #<Sketchup::Edge:0x0002667b2e83d8>
onElementAdded: #<Sketchup::Face:0x0002667b316e90>
onElementAdded: #<Sketchup::Edge:0x0002667b2e80b8>
onElementAdded: #<Sketchup::Face:0x0002667b2e8428>
onElementAdded: #<Sketchup::Edge:0x0002667b2e8d60>
onElementAdded: #<Sketchup::Face:0x0002667b2e9710>
onElementAdded: #<Sketchup::Face:0x0002667b2ea0c0>
onElementAdded: #<Sketchup::Edge:0x0002667b2d3f00>
onElementAdded: #<Sketchup::Face:0x0002667b2d3ca8>
onElementRemoved: 14245
onElementModified: #<Sketchup::Edge:0x0002667b44ab40>
onElementModified: #<Sketchup::Edge:0x0002667b44a8c0>
onElementModified: #<Sketchup::Edge:0x0002667b44a500>
onElementModified: #<Sketchup::Edge:0x0002667b44a3e8>

But when I PushPull such that I’m only moving a face:
image

onElementModified: #<Sketchup::Face:0x0002667b316e90>

From what I gather, you would also need some notification for the neighboring faces. Understandably.

I think that I would try to get the neighbouring faces from the EntitiesObserver. Though, I can see that you might not want to do that for every face. Initially I thought that we could perhaps leverage the ToolsObserver to know when a PushPull was performed (but face.pushpull complicates that).

Need to ponder about this one… I really don’t think observing every Loop will be viable in terms of performance.

The challenge is that EntitiesObserver will notify about about a single face during a push pull. With such a pushpull it’s actually the vertices being changed - moved. And these aren’t propagated through the EntitiesObserver.

Btw, I logged a feature request in regard to the possibility of having vertices and loops included in EntitiesObserver: SU-38061

In my opinion, list of entityID’s of all affected entities (or hash {:addedEntities, :modifedEntities, :removedEntitiIds}) in onTransactionCommit will be much better for all rendering-type plugins…

We’ll not need to use any observers.
Of course it should contains also ids of side-modified faces (or loops) in case of push-pull (You probably have something like this to be able to update SU geometries during rendering).

Yes, exactly, I also think that there is another issue in case of cutting hole in face.
(I’m not sure now - I need to reproduce this issue but it’s more or less when we make inner loop and removing its interior there is no ElementModified on face)

Very good point. I think we might have such a list internally. Might be something to leverage. I’ll add your comment to the issue I logged.

How it may affect performance? For example selection contains about a thousand of triangle faces and user erases all of them at once by hitting Del key. Would it be noticeably slower compared to a current state?
I have Thea Render and from my observations it begins to refresh rendering of the whole model even after a single object erasing/modification/creation (maybe because global illumination of a model changes even because of a single object), so looks like it is unnecessary to know ids of affected objects in such case. It is enough to know that something is changed in order to trigger recalculation.
[UPD]Or maybe it is just because it is technically challenging to figure out ids of affected objects at the moment.

I think it is possible to use suggested solution with selection observer and just force refreshing of all faces adjacent to a push-pulled one instead of trying to figure out were they affected or not after push-pulling of selected one (most likely they were). Maybe it may cause some false positives (so refreshing of some non-affected adjacent faces may take place), but feels like it may work fine in most cases if I understood a task correctly.
[UPD]Seems like selection doesn’t change while push-pulling (despite a fact that a face being push-pulled looks like selected from a user’s point of view), so looks like it is actually better to use EntitiesObserver instead. And force recalculation of all faces adjacent to added/modified ones.

To ensure you only have ONE observer attached to some entity…
Make one reference to it in your methods - say - @myobserver
Before attaching it you can try to remove it - the remove fails silently if it’s not attached already…
So:

entity.remove_observer(@myobserver)
entity.add_observer(@myobserver)

Now you’ll only get one instance of that observer attached to the entity.

This is (supposedly) always true, by API design. Viz:

The possibility exists that in early versions the attachment code was bugged.


What I had hoped Thomas or Bugra would state here publicly, is what Bugra told me privately some cycles ago. So since neither of them have, I’ll restate it (from memory) and then @tt_su or @bugra can confirm or deny.

So, I (think I remember) was told, when I asked specific questions about the add_observer() method and the observer callback queue is, that calling the add_observer() method on an entity, that is already being observed (ie “attached to that entity”,) will first remove the specific observer attachment from it’s current position in the queue, and then re-attach it at the end of the queue.

I had asked that the docs be updated to state this little nugget of information, but they have not (yet).