Shrinking the Image

(last updated: Mar 14, 2005)

Shrinking of the image is a process that is occasionally performed to remove unwanted classes. The intention is often to get a small image that can be used on equipment with very limited memory. In principle you can remove classes manually one by one, but shrinking does more: It finds unsent methods and removes them, too. (It would be difficult to do that manually: Squeak 3.4 contains more than 41000 methods!)

Methods that support shrinking are located in the instance protocol of SystemDictionary in the message categories 'housekeeping' and 'shrinking'. (we do not currently have a Shrinker class.) Dan Ingalls, Scott Wallace, Michael Rueger, Stefan Matthias Aust, Andreas Raab, Tim Rowledge, Jon Hylands, Bob Arning, Gerald Leeb and possibly others contributed ideas and code to the shrinking procedure. Jon Hylands published a shrinking script for Squeak 3.4 that removes the Morphic interface and some other features. (It is known that Jon uses Squeak on equipment with very limited resources.) Jons script removes also the development tools which are not needed on his equipment. I used Jons code to derive a script that creates a MVC-only image that keeps the development tools. That image is designed to meet the needs of schools that are not equipped with the most powerful (and most expensive) computers.

Possible Problems

Often the image shrinking produces an image that contains obsolete classes. An obsolete class is the remnant of a removed class. An obsolete class continues to exist in the image for as long as

There are two approaches to this problem: One can try to remove obsolete classes from a shrunken image - this is difficult, but possible - and one can try to modify the image prior to shrinking it in such a way that the shrinking will not leave obsolete classes in the image. This second approach is to be preferred.

Compiled methods that reference removed classes cannot successfully be recompiled - a recompilation helps to find such methods very quickly. References to instances of removed classes are much more difficult to find. It is a good idea to inspect global dictionaries, pool dictionaries, class variables and class instance variables before you shrink the image.

During a preparatory step to shrinking one has to modify all methods that contain references to classes that are to be removed. This is not an easy task: It requires a lot of patience to disentangle MVC and Morphic - which is necessary when you want to remove the Morphic interface.

It is certainly helpful to give an example. The instance protocol of Model contains this method:

containingWindow
    "Answer the window that holds the receiver. 
     The dependents technique is odious and may 
     not be airtight, if multiple windows have
     the same model."

   ^self dependents detect:
          [:d | ((d isKindOf: SystemWindow orOf: StandardSystemView)
                     or: [d isKindOf: MVCWiWPasteUpMorph])
                and: [d model == self]
          ] 
       ifNone: [nil]

The classes shown in red are part of the Morphic system. Assume that we want to remove the Morphic interface and that we leave this method as it is. This method alone would obstacle the complete removal of the classes SystemWindow and MVCWiWPasteUpMorph. It is also impossible to remove the superclasses of classes that cannot be removed. Therefore the shown method alone will cause the retention of obsolete classes for all these classes:

Morph 
  BorderedMorph
    MorphicModel 
      SystemWindow 
    PasteUpMorph 
      WiWPasteUpMorph 
        MVCWiWPasteUpMorph

When we wish to remove the Morphic system and to keep the MVC interface, we have to rewrite the shown method in this way:

containingWindow
    "Answer the window that holds the receiver. 
     The dependents technique is odious and may 
     not be airtight, if multiple windows have
     the same model."

  ^self dependents
    detect: [:d | (d isKindOf: StandardSystemView)
                    and: [d model == self]]
    ifNone: [nil]

StandardSystemView is part of the MVC interface.

When we wish to remove the MVC interface and to keep only the Morphic interface, a different modification is required:

containingWindow
    "Answer the window that holds the receiver.
     The dependents technique is odious and may
     not be airtight, if multiple windows have
     the same model."

   ^self dependents detect:
          [:d | ((d isKindOf: SystemWindow)
                     or: [d isKindOf: MVCWiWPasteUpMorph])
                and: [d model == self]
          ] 
       ifNone: [nil]

Your see that this is real work.

A newer idea, the ToolBuilder, will greatly simplfy this problem: The ToolBuilder allows you to describe a window in terms of class-independent specifications. A builder is used to translate these specifications into a MVC window or into a Morphic window - whatever is needed.

Change sets typically contain refrences to classes, even to deleted classes. Classes that are referenced from a change set cannot be completely removed during shrinking and will converted into obsolete classes. To avoid this, one has to close all change sorters and to destroy all change sets before shrinking starts.

Preparatory Changes of the Image:

It is necessary to set all preferences to values that can be used in the shrunken image. To give an example, you have to set all font preferences to strike fonts before you can remove the TTF font support.

The Shrinking Script

The shrinking script is designed to completely remove the Morphic interface. It does several things:

First, it replaces a lot of methods that contain references to Morphic classes with newer version that do not reference classes that are later removed. Finding this methods was quite time consuming, but this is something that has to be done manually. The first step of shrinking also deletes some classes, but this is something that could be done later as well.

Second, the postscript of the script removes references to instances of Morphic classes from some global data structures. This is discussed in detail later on.

The last part of the shrinking is done by the user. To start it, the user executes these statements in a MVC-project:

Smalltalk preMajorShrink.
Smalltalk majorShrink.

After execution of these statements, the expression

Smalltalk obsoleteClasses size

should return zero. If it does not, you can inspect the expression Smalltalk obsoleteClasses to find out which classes left remnants in the image.

It is a good idea to merge the sources and the changes into one new sources file. You do this with:

Smalltalk condenseSources.

What can you do when you have obsolete classes in the image?

A very simple and helpful first step is to load the condensed sources into a text editor and to search the names of the obsolete classes one by one. This helps you to find methods that you have to rewrite. It is prudent to collect all changes that you do into a new change set. You can later file out that change set, merge it with the shrinking script and file out a modified (and normally improved) shrinking script.

When you cannot find references to obsoleted classes in the current sources, you have reason to assume that some global variable or some pool dictionary contains a reference to an instance of the obsoleted class. This is difficult to find in a shrunken image. You will better open the full image and search all instances of the class in question. Once you have all instances, you will have - for every instance - to follow all references to the instance. This can be really difficult. As an alternative, you may search sources and changes of the full image for methods that refer the class in question. With a bit of good luck, you will find a class method that initializes a pool dictionary or a global variable.

Important methods in class SystemDirectory

Details about the Postscript of the Shrinking Script:

Flaps disableGlobalFlaps: false.
Preferences setParameter: #HaloSpecs to: IdentityDictionary new.
Preferences removeFlaps.

ParagraphEditor initializeTextEditorMenus.
Smalltalk removeKey: #ActiveEvent;
   removeKey: #ActiveHand;
   removeKey: #ActiveWorld;
   removeKey: #ChessConstants.

Vocabulary resetVocabulary.
ExternalDropHandler resetRegisteredHandlers.
DocLibrary resetServers.

ExternalSettings shutDown.
ExternalSettings registeredClients asArray do:
      [:item | ExternalSettings registeredClients remove: item].

ServerDirectory resetLocalProjectDirectories.
ServerDirectory resetServers.

Flaps dropStandardFlaps.

Here, the classes ServerDirectory and ExternalSettings are important:

Note that the statements of the postscript are executed when the change set is filed in. When you can edit the postscript, things are already done! To remove unwanted statements from the postscript, you proceed as follows:

About the Algorithms used for Shrinking

First, packages and classes are deleted. This is the simple part of shrinking and it is something that you could do manually as well. The sophisticated part comes later. It is a loop. During the loop, all unsent messages are searched and deleted. Deletion of all unsent messages may remove all references to a previously referenced class. When a class is not referenced from any method, it is considered unused if it has no subclasses. These unused classes are searched and removed. Removal of classes may render methods in other classes unused and therefore the search-and-remove algorithm is repeated until no unused methods are found.

A method is considered as used, if it is sent from at least one other method or if its selector is referenced from at least one other method. Regrettably, there are programming styles, that hide the use of methods and the image contains some examples of code that hide the use of methods from the shrinker. Here are some examples:

In PackageLoader>>fileInFrom: we read:

fileInFrom: aStream
    | chgRec changes |
   changes := (ChangeList new
                  scanFile: aStream
                  from: 0
                  to: aStream size) changeList.
   aStream close.
   ('Processing ', self packageName)
       displayProgressAt: Sensor cursorPoint
       from: 1
       to: changes size
       during:[:bar|
                1 to: changes size do:
                    [:i|
                        bar value: i.
                        chgRec := changes at: i.
                        self perform: (chgRec type copyWith: $:) asSymbol
                             with: chgRec.
                    ].
              ].

The statement shown in red constructs a message selector by appending a colon to a string which is then converted into a symbol. The shrinking algorithm is unable to find out what methods are really used and it is not a surprise that an earlier version of the shrinker removed the method method: which is needed, but not sent from any other place in the image. To fix that problem, we have to include the selector #method: into the shrink support method presumedSentMethods

The following example is taken from class Debugger:

preDebugButtonQuads

  ^#(('Proceed'   proceed   blue   'continue execution' )
     ('Abandon'   abandon   black  'abandon this execution by closing this window')
     ('Debug'     debug     red    'bring up a debugger'))

This array of arrays is used elsewhere to create the buttons of the error notification window. The parts shown in read are message selectors that the shrinking algorithm cannot find. The messages proceed and debug are sent elsewhere and will not be removed, but method abandon will be removed as unsent. To avoid that, we have to include its selector into the shrink support method presumedSentMethods

Inclusion of important unsent methods

Some very important methods are not referenced from methods in the image and have to be included into method presumedSentMethods to ensure their inclusion into the image. These are:

The inclusion of other unsent methods is desirable. Candidates for inclusion are:

Inclusion of all subclasses of FileDirectory

It is recommended to include all subclasses of class FileDirectory into a shrunken image. These are:

FileDriectory
  AcornFileDirectoy
  DosFileDirectory
  MacFileDirectory
    MacHFSPlusFileDirectory
  UnixFileDirectory

When you remove some of these subclasses, Squeak looses its cross-platform portability.

The method SystemDictionary>>majorShrinkClassesToZap enumerates the classes that will be removed from the image. Ensure that for a cross-platform portable Squeak, none of the directory classes is mentioned there.

Selected Shrinking Scripts

Version
Scripts
Squeak 3.4Shrinking scripts for Squeak 3.4
Squeak 3.6Shrinking scripts for Squeak 3.6
Squeak 3.7 #5989Shrinking scripts for Squeak 3.7