How to chase pointers with VMMaker? (was: How to clean up dangling objects?)
"Thiede, Christoph via Squeak-dev" <[email protected]>
| Newsgroups | gmane.comp.lang.smalltalk.squeak.general,gmane.comp.lang.smalltalk.squeak.vm.devel |
|---|---|
| Message-ID | <GV2PR09MB83715BECEFA3A9C7A259D3ADFB0E2@GV2PR09MB8371.eurprd09.prod.outlook.com> |
Hi Eliot, all, I'm facing similar issues with dangling pointers again in my image: | obj | obj := Object new. wa := WeakArray with: obj. Inspector openOn: obj. "close the inspector, purge undo records, run GC" wa first class. --> still Object, not UndefinedObject I cannot reproduce this in a fresh trunk image, so it's either related to some functional changes in my image (of which there are, unfortunately, loads) or to a mysterious bad state of my object memory that survives snapshots and resumes. Neither PointerExplorer nor PointerFinder can reveal the cause of the dangling reference. I have not exhausted all possibilities of bisection (i.e. gradually reverting the changes in my image) but it would take eternities and because I cannot trust PointerExplorer and PointerFinder I'm kind of experimenting blindly. How can I investigate this issue with VMMaker and find out why an oop has not been gc'ed? Is there a productive/practical workflow for this? I am happy to do it by myself if you can guide me, but otherwise I could also send you a 13 GB image file off-list should that be easier for you than explaining the process. Also happy to meet if you prefer, in this case I can share the learnings later on the list. I would really appreciate any help! Best, Christoph ________________________________ Von: Christoph Thiede via Squeak-dev <[email protected]> Gesendet: Montag, 16. März 2026 19:34 Uhr An: [email protected] <[email protected]> Cc: Thiede, Christoph <[email protected]> Betreff: [squeak-dev] Re: How to clean up dangling objects? Hi Dave, my latest state is already described in this conversation, I'm afraid. I was able to identify a few arrays that were preventing the collection of a very large number of other objects and manually nilled their elements to release the other objects. In general, the following strategies, roughly in this order, seem useful for me for investigating the reasons for huge images. Likely you already know most of these strategies, but I am listing them for sake of completeness anyway. Maybe we can turn this into a wiki article in the future: Overall, you want to answer the following three questions: 1. What objects make my image so large? 2. Why are they not garbage-collected? 3. How can I get rid of them? The first and most obvious strategies are: - Manually invoke the garbage collection (GC) from the Extras menu. In some cases, running it twice in a row is said to be helpful to trigger pending finalizations (I am not sure about this, but e.g. the implementation of EphemeronIdentityDictionaryTest>>#triggerFinalization hints to this). - Purge undo records from the Extras menu if recommended by the post-GC pop-up dialog and rerun the GC. - Close unnecessary windows or unnecessary projects from the Projects menu. If you want not only to fix but also to analyze the situation, rerun the GC after every new chunk of closed windows or projects to figure out which window or project contained the unwanted references. - In rare cases, snapshot and quit the image and restart the VM (the VM seems not always to release freed memory areas to the operating system immediately; a restart can rule out memory leaks by any plugins or FFI code; and in some cases, the shutDown/startUp routines might clean up volatile data). If none of the above helped, the following strategies can help to answer the first question (what objects make my image so large?): - Run a full space analysis (from Help > About Squeak) and skim it for suspicious/anomalous records. For instance, thousands of instances of a class from a third-party package could be a symptom that this package has left dangling instances in the system. Beware that for larger images, the space analysis can take several minutes or longer. You can also compare the result to that from an empty trunk image to identify anomalous patterns (cf SpaceTally>>#printSpaceDifferenceFrom:to:). - As an approximative alternative to a full space analysis, inspect a sample of random objects: | objects sample | objects := self systemNavigation allObjects. sample := (1 to: 1000) collect: [:i | objects atRandom]. (sample collect: [:ea | ea class] as: Bag) sortedCounts explore. If you have found classes with a suspicious high number of instances: - If these classes have several purposes (such as collections or strings), inspect random instances to understand what they actually contain/relate to: String allSubInstances atRandom inspect. Or, inspect a sample of many: | objects sample | objects := String allSubInstances. sample := (1 to: 50) collect: [:i | objects atRandom]. sample explore. Beware that you now have created a new reference on these objects by opening them in an inspector or explorer window. Before proceeding with the second question, make sure to close these windows, purge undo records, and re-trigger the GC (see above). - After performing any clean-ups in your image, you can also track the number of instances in your imege without rerunning the full space analysis: ByteString allInstances size. At the end of this step, you should know one or multiple objects that you think should have been GC'ed, or classes of objects that you think most instances of them should have been GC'ed. To continue with the second question (why are these object not garbage-collected?): - Chase the pointers of an object that you think should have been GC'ed by using the PointerFinder. Note that if you are watching the object in an inspector/explorer or have assigned it to a variable in a debugger or workspace, this will constitute a reference to the object on its own and might establish the reference chain reported by the PointerFinder, possibly masking another actually relevant reference chain. Thus, avoid referencing the object directly but reference it indirectly. E.g.: Association allInstances atRandom chasePointers. (World actionMap at: #aboutToEnterWorld) "a WeakMessageSend" receiver chasePointers. Beware that chasing pointers may take a couple of minutes in larger images. The resulting PointerFinder window should educate you about a chain of references from the root of the image to the object in question. Inspect each node within this list and decide whether it should have been GC'ed or not. If you have found the first node that should have been GC'ed, you have found the unwanted reference. Often, this is a reference you should have wanted to clean up when stopping some application or closing some tool (e.g., in some #windowIsClosing or #outOfWorld: method), or a strong reference where a weak reference would have sufficed (see strategies for holistic fixes below). Another frequent reason are bindings in a workspace or interactive print-it results (see strategies for manually eliminating dangling references below). If the resulting PointerFinder window is empty, you have made an interesting observation. Either the object in question has already been GC'ed after the invocation of the PointerFinder (e.g., | tmp | tmp := Object new. tmp chasePointers), or it is only referenced by a long-running context of the current call stack, or there is a cycle of weak references involving that object (see below on holistic fixes), or you have found a bug in either the GC or the PointerFinder. If the PointerFinder discovers either no chain of references or an irrelevant one, you can continue by exploring all the ingoing references to the object: - Browse the references pointing to an object that you think should have been GC'ed by using the PointerExplorer. Like above, beware of irrelevant references to the object from your currently open tools, which will pollute the resulting PointerExplorer (yet they are not masking relevant findings as opposed to when using the PointerFinder): Association allInstances atRandom explorePointers. Navigate through the inverse reference graph by expanding the nodes in the resulting PointerExplorer window. Watch out for global state that is intentionally persisted in the image, such as PasteUpMorph or Project instances, class objects, globals such as Smalltalk, or the special objects array. Ignore weak references (rendered in gray). Explore context objects or block closures after any other references, as they are often (but not always!) of a temporary nature. If you have found the first node that should not have been GC'ed, you have found the unwanted reference. See instructions above on PointerFinder on how to continue with that. If you have found a leaf node (one that has no further references), that might or might not be interesting. Either the selected object was already GC'ed (common for short-living references e.g. from tool code), or it is only referenced by a long-running context of the current call stack, or you have found a bug in either the GC or the PointerExplorer. If you have found a node that has only weak ingoing references, the same possible reasons apply. To address the third question (How can I get rid of these objects?), you have to decide between two goals: do you want to fix a recurring issue once for all (holistic fix), or do you only want to get rid of your current objects (e.g., because they were created due to some earlier already fixed code)? Strategies to accomplish the first goal (holistic fix): - Make sure to clean up unneeded global references to objects from your application when it is stopped or closed. For instance, if your morph registers itself in a properties of the world, remove that property when the morph disappears. Common hooks to use for this include #outOfWorld:, #windowIsClosing, and #ensure:/#ifCurtailed: blocks for everything that should be cleaned up even/if a piece of code does not terminate regularily but is interrupted due to an error/manual process termation. - Use weak references instead of strong references where appropriate, e.g., for observing purposes when an object should only be notified as long as it exists. See WeakArray, EphemeronIdentityDictionary et al. on how to use weak references. - If there are cyclic dependencies of weak references, manually break them up when an application is stopped. For instance, when using #update: (Object>>addDependent: et al.) *outside of a Model subclass* or Object Events (#when:send:to: et al.), send #release to the model in #windowIsClosing. If this strategy is succesful, you likely have found a good reason to migrate to Ephemeron(Identity)Dictionary. Strategies to accomplish the second goal (manually eliminate dangling references): - Check for dangling references in the bindings dictionary of a workspace. If a workspace is configured to automatically declare variables, it may hold the intermediate results of do-it scripts that were meant to be temporary. Those are especially hard to spot if the containing tool window was collapsed or moved into a different project. To clean up unwanted workspace bindings, you can either reassign the relevant variables through another do-it in the workspace, or use "reset variables" from the window menu of the workspace. - Check for dangling interactive print-it results (that are styled as a clickable blue link). These may refer to an unwanted object and are revealed through a TextInspectIt instance in a PointerFinder or PointerExplorer window. To clean up unwanted references from interactive-prints, you can either delete them or select them and press Cmd + 0 to remove the inspection attribute from the text. - Manually nil out variables or fields pointing to the unwanted object (like owner := nil). - Manually detach the object by forwarding it to nil: anObject becomeForward: nil. The latter can be dangerous, as any existing variable in other objects pointing to anObject will be changed to nil, potentially breaking the behavior of those other objects. Consider forwarding to a safer null object from the domain instead if it makes sense (such as an empty list or string, a NullStream, a fresh instance of the same class, etc.). Other things to check (which are not suspicious per se, but perhaps related to known or possible bugs right now): - Does the image contain any proxies? (They are currently not properly supported by SpaceTally and PointerFinder/PointerExplorer.) (self systemNavigation allObjects reject: [:ea | (thisContext objectClass: ea) includesBehavior: Object]) explore. Ignore any instances of MCPackage in the resulting list. This is a known issue (well, I just spotted it today - we can discuss this somewhere else). - Does the image use any ephemerons? (They are currently not properly supported by PointerFinder/PointerExplorer.) Ephemeron allSubInstances notEmpty. If believe you have found a bug, send a bug report to the squeak-dev list or bugs.squeak.org, ideally providing steps to reproduce or preparing to share an image in private. On a specific note, if your image contains Squeak Inbox Talk/SqueakHistory and has loaded many years of mails, this can also consume some memory. You can clear text caches non-destructively via SqhMailWrapper allInstancesDo: [:ea | ea removeProperty: #plainText; removeProperty: #richText] or clear all mail caches via TalkInbox clearAllCaches (beware that the latter will invalidate any open Squeak Inbox Talk windows and downloading all messages from over two decades again might take several hours, and optionally cost ~$0.50 for rebuilding the semantic corpus if you use the AI features and have provided an OpenAI key). Oops, this got quite long. Hope anything of this might be helpful. If not, maybe we can at least improve and reuse this document as a wiki article. Feedback welcome. Otherwise, we could also take a look at your image after the next board meeting if you want. :-) Best, Christoph -- Sent from Squeak Inbox Talk<https://github.com/hpi-swa-lab/squeak-inbox-talk> On 2026-03-14T17:16:40-04:00, [email protected] wrote: Hi Christoph, I am curious if you may have any further updates on this subject. My own working image, based on Squeak 6.0 and kept up to date with trunk, is now more than 9GB is size, and it does not want to get smaller even if I delete all of my projects, clear MC caches, and so forth. Thanks, Dave --- Sent from Squeak Inbox Talk On 2026-02-21T02:56:57+01:00, christoph.thiede(a)student.hpi.uni-potsdam.de wrote: > On 2026-02-20T21:29:09+01:00, christoph.thiede(a)student.hpi.uni-potsdam.de wrote: > > > Hi Eliot, all, > > > > I have been further investigating this situation with limited success. What was fruitful was an "amnesty" for all PointerFinder and PointerExplorer objects because my chase repeatedly ended in an array of SystemNavigation allObjects I was storing in my version of the PointerExplorer. While this array referenced the PointerFinder and the other way around, neither of them should be reachable from the outside. Anyway, the following amnesty could release that: > > > > PointerFinder allInstancesDo: [:ea | > > 1 to: ea class instSize do: [:i | > > ea instVarAt: i put: nil]]. > > PointerExplorer allInstancesDo: [:ea | > > 1 to: ea class instSize do: [:i | > > ea instVarAt: i put: nil]]. > > PointerExplorerWrapper allInstancesDo: [:ea | > > 1 to: ea class instSize do: [:i | > > ea instVarAt: i put: nil]]. > > > > Now my image is down from 12 GB to 5.3 GB. But that's still too large, and I still have about 20 dangling PointerFinder instances in my image, for which explorePointers finds nothing meaningful and chasePointers finds nothing at all. > > > > I want to tackle this issue one for all and fix/document PointerFinder and PointerExplorer or alternatively rule out a bug on the image side to suggest to a possible bug in the VM in this case. Below are a few questions, it would be really great if you could help me find answers to them. > > > > 1. I noticed that PointerFinder hard-codes Smalltalk and Processor as starting points for the search. I have changed this to Smalltalk specialObjectsArray instead. This fixes Smalltalk chasePointers (caused an infinite loop before), Smalltalk specialObjectsArray chasePointers (found nothing before), and (Smalltalk specialObjectsArray at: 27) chasePointers et al. (found nothing before). Is that fix correct? Are there any other possible roots? As I understand the implementation in the VM correctly after a first look, at least one other possible root would be the current call stack, but I believe we are excluding it voluntarily. > > > > 2. I believe Object>>outboundPointersDo: should not follow variable fields if the class is weak. Do you agree? Consider this example (in a workspace with automatic variable declarations): > > > > ef := {{Object new}}. > > wa := WeakMessageSend new receiver: ef first first. > > wa receiver chasePointers. > > > > This constructs a reference chain involving wa instead of ef (given the current hashing strategy of strings and arrays). I would assume that the image has been gc'ed before invoking the PointerFinder, so if the GC is correct, every object in the image should be reachable through a chain of strong references only at this point. So we can a) speed up the PointerFinder a bit and b) more importantly show only meaningful reference chains. If I want to know why an object was not gc'ed, seeing a weak reference to it does not answer that question. > > > > 3. I have been thinking twice about your suggestion with #outboundPointersDo:for: in your earlier message but am not yet convinced that we need it. Following my above assumption about a recent GC, the following implementation should suffice IMO: > > > > Object>>outboundPointersDo: aBlock > > "do aBlock for every object I point to, exactly how the garbage collector would. Adapted from PointerFinder >> #followObject:" > > > > aBlock value: self class. > > (self class isEphemeronClass ifTrue: [2 "skip key"] ifFalse: [1]) > > to: self class instSize do: [:i | aBlock value: (self instVarAt: i)]. > > self class isWeak ifFalse: "see 2." > > [1 to: self basicSize do: [:i | aBlock value: (self basicAt: i)]]. > > > > Rationale: If the key still exists after the GC, it must be reachable through any strong reference other than the key of a non-ephemeron. > > Okay, I think I found my own mistake here: I mixed up garbage collection and finalization. If an ephemeron responds to mourning without actually releasing the key, it might indeed be accessible only through an ephemeron. And this is exactly the case #outboundPointersDo:for: would catch by presenting a reference chain through the ephemeron if and only if no reference chain through other strong references was found, right? > > > > Like above, seeing an Ephemeron in a PointerFinder is not a helpful answer to my question why its key was not GC'ed. So advance the search and find another reference instead that is actually preventing the collection. Is my understanding of Ephemerons flawed or would this implementation suffice? > > > > 4. I think Object>>shouldFollowOutboundPointers should not test for #isLiteral. Do you agree? Consider this example (in a workspace with automatic variable declarations): > > > > x := #('hi'). > > x first chasePointers. > > > > #('hi') would be treated as a literal and skipped. If I understand correctly, #isLiteral should only be used for printing/serializing/encoding concerns and be documented accordingly. If I remove that check from #shouldFollowOutboundPointers, #chasePointers does not feel significantly slower. We could still manually exclude true false nil, scaled decimals, large integers, and boxed floats if desired, but I think it is not even worth the effort. Plus, without this check the implementation is more robust against accidental incorrect stores in these objects, such as a ScaledDecimal whose fraction is a proxy object on something else. > > > > 5. PointerFinder is currently not safe for transparent proxies of all kinds. #outboundPointersDo: and #shouldFollowOutboundPointers are implemented on Object not ProtoObject so a proxy on a non-pointer object would be excluded from chasing, even if it references any other pointer object. Analogously, if a class overwrote #class or #basicAt: or #instVarAt:, misbehavior or accidental exclusions would be possible. The comment in #shouldFollowOutboundPointers suggests that this is an explicit hook, but I am not convinced by this idea. I think PointerFinder could have two modes: (i) system-level pointer chasing where we care about the behavior of the VM, and (ii) high-level pointer chasing where domain objects such as remote proxies could join the game and explain reference chains among multiple images etc. If we have a need for (ii), I am happy to implement both modes. For (i), both selectors should either be moved to ProtoObject or to a static method and use mirror primitives. Analo gously, I think PointerFinder>>buildList should use mirror primitives to display objects. I have already implemented this in my imge and am happy to commit to the trunk if you agree. > > > > 6. Why does EphemeronIdentityDictionaryTest>>#triggerFinalization do so much more than a single GC run? Can I not generally trust Smalltalk garbageCollect to (i) release any unreferenced object and (ii) trigger the finalization process, or is that method a historic relict and can be simplified? > > > > I would appreciate your help a lot. 30 seconds GC every 5 minute for 8 hours a day are really a threat to my mental sanity. I'd love to fix this as fast as possible and definitely before the next release! I'm going to send other patches for PointerFinder and PointerExplorer to the inbox next. It would be great if you could review them. :-) > > Best, > Christoph > > --- > Sent from Squeak Inbox Talk Squeak-dev mailing list -- [email protected] To unsubscribe send an email to [email protected]