Re: How to chase pointers with VMMaker? (was: How to clean up dangling objects?)
Eliot Miranda <[email protected]>
| Newsgroups | gmane.comp.lang.smalltalk.squeak.vm.devel,gmane.comp.lang.smalltalk.squeak.general |
|---|---|
| Message-ID | <CAC20JE12KbFdfD04RoYMMBcH3uMVGVBJ4n1gZi34onKMiBrkjA@mail.gmail.com> |
On Thu, May 21, 2026 at 11:37 AM Thiede, Christoph <[email protected]> wrote: > 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. > There is a mistake in your example. You reference the new object via the temporary obj. Therefore it never dies before the entire doit complets. You need to write | obj | obj := Object new. wa := WeakArray with: obj. Inspector openOn: obj. *obj := nil.* "close the inspector, purge undo records, run GC" wa first class. --> still Object, not UndefinedObject to prevent the doit context preventing obj's reclamation. Beyond that, yes one could write a VMMaker pointer finder, and if the above is not your mistake then we could pair in writing such a tool, perhaps as early as next week. > 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 > > -- _,,,^..^,,,_ best, Eliot Vm-dev mailing list -- [email protected] To unsubscribe send an email to [email protected]