Removing entity observers
-
I add entity observers to each face, and later remove them, but this causes a slowdown each time I do it. An example demonstrates this (run on a file with 1000 cubes):
require 'sketchup' def timeOperation startTime = Time.now yield time = Time.now - startTime puts "Completed in; #{time.to_s}" end def addObservers Sketchup.active_model.entities.each { |e| if not e.instance_of? Sketchup;;Face o = Sketchup;;EntityObserver e.add_observer(o) e.remove_observer(o) # Returns true end } end def removeMaterials timeOperation { Sketchup.active_model.entities.each { |e| if e.instance_of? Sketchup;;Face e.material = nil e.back_material = nil end } } end
Running the methods in the Ruby console:
> removeMaterials Completed in; 0.172 > addObservers > removeMaterials Completed in; 0.78 > addObservers > addObservers > addObservers > addObservers > removeMaterials Completed in; 3.23
Each time addObservers is called, the time that removeMaterials takes increases. Shouldn't addObservers be a no-op? An observer is added and then removed, resulting in no observers. Also, this persists after a File->Open.
-
(1) You are supposed to subclass the Sketchup "Observer" superclasses, so that there is some ruby code within the callback methods, that executes when they get called.
Ie, you do not create direct instances of Sketchup::EntityObserver or any other Sketchup Observer superclass.(2) In an iterative block such as:
Sketchup.active_model.entities.each
you are actually iterating the C++ collection entities. Do not make changes when you do this, only get information. If you need to make changes, you must use an Array copy of the entities, like so:
Sketchup.active_model.entities**.to_a**.each
Otherwise the C++ side of Sketchup will shuffle the elements of the collection around on you while you are iterating it, and you either iterate the same elements multiple times, or miss elements entirely.(3) Your code:
if not e.instance_of? Sketchup::Face
would add observers to ALL objects except faces. You'd need to remove the negation keyword "not."(4) Your code is incorrect.. it does not actually add observers. quote:
o = Sketchup::EntityObserver
only creates an alias that points at the Sketchup::EntityObserver class definition. It does not point at an observer instance. In Ruby, you use instances of class objects.(5) Some developers use a single custom observer instance, to watch multiple objects. It may not be efficient memory wise, to create an individual observer instance, for each and every Sketchup::Drawingelement subclass object. (Not to say that there may be situations where it must be done.)
(6) In your method
addObservers
, (provided that you correct the code to actually attach an real observer instance,) it would do no good to attach an observer, and then immediately, remove it in the next statement.(7) Your statement at the top:
require 'sketchup'
is not needed unless you are calling a method in that file, or accessing a data structure defined in that file, and you wish to be sure that these things pe-exist before you use them.
It does not hurt, however, as Ruby will just check the $LOADED_FEATURES array and if it's been loaded, Ruby will do nothing, and go to the next statement.(8) Most important... Your code is not wrappped in a namespace (a module.) When run, it is evaluated within Object, the ancestor of ALL objects. Your methods will get inherited by EVERY object, as a private method. This wastes memory. Use a module name you can remember, like: Dmactest for playing around.
module Dmactest class << self # paste your methods here end # self end
(9) For large operations, you should have the UI stop, by using the model instance methods:
model.start_operation
and
model.commit_operation
See also:
model.abort_operation -
(10) And... with your method, if you use a local reference o, to refer to your observer instances, they (the references, not the observer instances,) will get disposed of when the method ends. Then you will not be able to reference them later (when you wish to remove them.)
You need to have the reference stored outside the method, as a @@ module reference, which can be a Hash, or an Array, if you need to hold references to multiple objects (either many observer instances, or many object references that one observer is attached to.) Ex:
@@watched_faces=[]
-
Thanks for the reply. I've updated the code but seeing the same results. I want to be able to remove observers, especially when a new model is opened. It does look like they're removed because the observer methods aren't called, but there is a slowdown each time they are added/removed which I don't get.
-
I did use a subclass of EntityObserver but removed it when creating the test. I've readded a trivial observer.
-
Ok. What causes this? Is it just calling methods on entities, or adding/removing entities? or any mutable operation on the model?
-
I wanted to show that it doesn't matter what entities the observers are added to, the effect is the same.
-
Yes, I meant to create an instance.
-
That sounds better but I can't seem to get removeObservers to work with it. When I tried using a single instance with add_observer/remove_observer (@@observer in the below code), the puts output looked correct, but only a single observer was removed. remove_observer returns false, and I see the observer output when I paint materials.
6, 10) I did this to simplify the example. It's the same when they are added and removed at separate times.
-
Ok.
-
Ok, I already use modules for larger scripts. What memory is used though? I thought each method used a constant amount of memory, no matter how many instances or subclasses there are.
-
Ok.
The results now are:
removeMaterials
(Completes in 0.05)addObservers
(Observers are added. Painting a surface prints observer output.)removeObservers
(Observers are removed. remove_observer returns true. Painting a surface gives no output.)removeMaterials
(Completes in 0.4)removeMaterials takes more time after calling addObservers/removeObservers again. What is taking the time if the observers aren't active?
module Dmactest class MyEntityObserver < Sketchup;;EntityObserver def onEraseEntity(e) puts "Erase Entity; #{e}" end def onChangeEntity(e) puts "Change Entity; #{e}" end end class << self @@watchedFaces = [] @@observer = MyEntityObserver.new def timeOperation startTime = Time.now yield time = Time.now - startTime puts "Completed in; #{time.to_s}" end def addObservers model = Sketchup.active_model removeObservers @@watchedFaces = model.entities.select { |e| e.instance_of? Sketchup;;Face } @@watchedFaces.map! { |f| [f, MyEntityObserver.new] } @@watchedFaces.each { |f, o| # puts "Adding Observer; #{f} #{@@observer}" # f.add_observer @@observer puts "Adding Observer; #{f} #{o}" f.add_observer o } return nil end def removeObservers model = Sketchup.active_model @@watchedFaces.each { |f, o| # puts "Removing Observer; #{f} #{@@observer}" # puts "Result; #{f.remove_observer @@observer}" puts "Removing Observer; #{f} #{o}" f.remove_observer o } @@watchedFaces = [] return nil end def removeMaterials model = Sketchup.active_model timeOperation { model.start_operation 'removeMaterials', true Sketchup.active_model.entities.to_a.each { |e| if e.instance_of? Sketchup;;Face e.material = nil e.back_material = nil end } model.commit_operation } end end end
-
-
@dmac said:
- Ok. What causes this? Is it just calling methods on entities, or adding/removing entities? or any mutable operation on the model?
Only changes to objects (entities,) in the model. You can call query methods directly that don't change the entities, such as getting a material, or it's visible state.
@dmac said:
- Ok, I already use modules for larger scripts. What memory is used though? I thought each method used a constant amount of memory, no matter how many instances or subclasses there are.
The method itself, yes... but each instance has to hold a pointer to the method in the superclass. Using a module prevents ALL objects from inheriting pointers to methods that they will never use.
When you define global methods that should not be global, you invade other developer's namespaces (their modules and classes,) as well as all of Ruby's standard modules and base classes.
-
Some of the Observers are bugged:
Review this topic: State of Observers
-
@dmac said:
removeMaterials takes more time after calling addObservers/removeObservers again. What is taking the time if the observers aren't active?
Some of us are suspicous about the remove_observer()
(I recall some other topics, here at SCF where other people reported slow downs as the number of model entities increased. Do a search on this forum.)When you are done with an observer instance, set it to nil and run Garbage Collection.
GC.startAlso:
@@watchedFaces.each { |f, o|
needs to be:
@@watchedFaces.each_with_index { |f, o|
if you are using an Array...... but I think you want to use a Hash instead, where the keys are the object_id of the face objects, and the Hash values are references to the observer instance that is attached to it.
@@watchedFaces = Hash.new model.entities.to_a.each { |e| (@@watchedFaces[e.object_id]=MyEntityObserver.new) if e.instance_of?(Sketchup::Face) }
(Since the entity is getting a 'change' by attaching an observer, it is smart, to iterate an array, rather than the C++ entities collection directly.)If there are Groups and Components in the model, then the above will only find "free" face objects, not faces nested down in Group or Component hierarchies.
Advertisement