summaryrefslogtreecommitdiff
path: root/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd
diff options
context:
space:
mode:
authorradiohotline <radoihotline@disroot.rg>2026-08-25 16:33:56 +0300
committerradiohotline <radoihotline@disroot.rg>2026-08-25 16:33:56 +0300
commita89e744ed0e8c759caf041a580112365bbfe4303 (patch)
treedd61e320904176995727e86a857d00c90d4933ba /addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd
parent6d53d8cac78250dc888aa49d5f9d4175c6652feb (diff)
escoria updateHEADmain
Diffstat (limited to 'addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd')
-rw-r--r--addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd135
1 files changed, 135 insertions, 0 deletions
diff --git a/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd
new file mode 100644
index 0000000..dd03a31
--- /dev/null
+++ b/addons/gdUnit4/src/core/execution/GdUnitMemoryObserver.gd
@@ -0,0 +1,135 @@
+## The memory watcher for objects that have been registered and are released when 'gc' is called.
+class_name GdUnitMemoryObserver
+extends RefCounted
+
+const TAG_OBSERVE_INSTANCE := "GdUnit4_observe_instance_"
+const TAG_AUTO_FREE = "GdUnit4_marked_auto_free"
+const GdUnitTools = preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
+
+
+var _store :Array[Variant] = []
+# enable for debugging purposes
+var _is_stdout_verbose := false
+const _show_debug := false
+
+
+## Registration of an instance to be released when an execution phase is completed
+func register_auto_free(obj :Variant) -> Variant:
+ if not is_instance_valid(obj):
+ return obj
+ # do not register on GDScriptNativeClass
+ @warning_ignore("unsafe_cast")
+ if typeof(obj) == TYPE_OBJECT and (obj as Object).is_class("GDScriptNativeClass") :
+ return obj
+ #if obj is GDScript or obj is ScriptExtension:
+ # return obj
+ if obj is MainLoop:
+ push_error("GdUnit4: Avoid to add mainloop to auto_free queue %s" % obj)
+ return
+ if _is_stdout_verbose:
+ print_verbose("GdUnit4:gc():register auto_free(%s)" % obj)
+ # only register pure objects
+ if obj is GdUnitSceneRunner:
+ _store.push_back(obj)
+ else:
+ _store.append(obj)
+ _tag_object(obj)
+ return obj
+
+
+# to disable instance guard when run into issues.
+static func _is_instance_guard_enabled() -> bool:
+ return false
+
+
+static func debug_observe(name :String, obj :Object, indent :int = 0) -> void:
+ if not _show_debug:
+ return
+ var script :GDScript= obj if obj is GDScript else obj.get_script()
+ if script:
+ var base_script :GDScript = script.get_base_script()
+ @warning_ignore("unsafe_method_access")
+ prints("".lpad(indent, " "), name, obj, obj.get_class(), "reference_count:", obj.get_reference_count() if obj is RefCounted else 0, "script:", script, script.resource_path)
+ if base_script:
+ debug_observe("+", base_script, indent+1)
+ else:
+ @warning_ignore("unsafe_method_access")
+ prints(name, obj, obj.get_class(), obj.get_name())
+
+
+static func guard_instance(obj :Object) -> void:
+ if not _is_instance_guard_enabled():
+ return
+ var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id()))
+ if Engine.has_meta(tag):
+ return
+ debug_observe("Gard on instance", obj)
+ Engine.set_meta(tag, obj)
+
+
+static func unguard_instance(obj :Object, verbose := true) -> void:
+ if not _is_instance_guard_enabled():
+ return
+ var tag := TAG_OBSERVE_INSTANCE + str(abs(obj.get_instance_id()))
+ if verbose:
+ debug_observe("unguard instance", obj)
+ if Engine.has_meta(tag):
+ Engine.remove_meta(tag)
+
+
+static func gc_guarded_instance(name :String, instance :Object) -> void:
+ if not _is_instance_guard_enabled():
+ return
+ await (Engine.get_main_loop() as SceneTree).process_frame
+ unguard_instance(instance, false)
+ if is_instance_valid(instance) and instance is RefCounted:
+ # finally do this very hacky stuff
+ # we need to manually unreferece to avoid leaked scripts
+ # but still leaked GDScriptFunctionState exists
+ #var script :GDScript = instance.get_script()
+ #if script:
+ # var base_script :GDScript = script.get_base_script()
+ # if base_script:
+ # base_script.unreference()
+ debug_observe(name, instance)
+ (instance as RefCounted).unreference()
+ await (Engine.get_main_loop() as SceneTree).process_frame
+
+
+static func gc_on_guarded_instances() -> void:
+ if not _is_instance_guard_enabled():
+ return
+ for tag in Engine.get_meta_list():
+ if tag.begins_with(TAG_OBSERVE_INSTANCE):
+ var instance :Object = Engine.get_meta(tag)
+ await gc_guarded_instance("Leaked instance detected:", instance)
+ await GdUnitTools.free_instance(instance, false)
+
+
+# store the object into global store aswell to be verified by 'is_marked_auto_free'
+func _tag_object(obj :Variant) -> void:
+ var tagged_object: Array = Engine.get_meta(TAG_AUTO_FREE, [])
+ tagged_object.append(obj)
+ Engine.set_meta(TAG_AUTO_FREE, tagged_object)
+
+
+## Runs over all registered objects and releases them
+func gc() -> void:
+ if _store.is_empty():
+ return
+ # give engine time to free objects to process objects marked by queue_free()
+ await (Engine.get_main_loop() as SceneTree).process_frame
+ if _is_stdout_verbose:
+ print_verbose("GdUnit4:gc():running", " freeing %d objects .." % _store.size())
+ var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, [])
+ while not _store.is_empty():
+ var value :Variant = _store.pop_front()
+ tagged_objects.erase(value)
+ await GdUnitTools.free_instance(value, _is_stdout_verbose)
+ assert(_store.is_empty(), "The memory observer has still entries in the store!")
+
+
+## Checks whether the specified object is registered for automatic release
+static func is_marked_auto_free(obj: Variant) -> bool:
+ var tagged_objects: Array = Engine.get_meta(TAG_AUTO_FREE, [])
+ return tagged_objects.has(obj)