Commits

dirkbaechle  committed a8570fa

Added release_target_info() to File nodes, reduces memory consumption.

  • Participants
  • Parent commits c576ba6

Comments (0)

Files changed (9)

File src/CHANGES.txt

       SCons from a source (non-installed) dir.
     - Count statistics of instances are now collected only when
       the --debug=count command-line option is used (#2922).
+    - Added release_target_info() to File nodes, which helps to
+      reduce memory consumption in clean builds and update runs
+      of large projects.
 
   From Gary Oberbrunner:
     - Test harness: fail_test() can now print a message to help debugging.

File src/engine/SCons/Executor.py

         self.action_list = action
 
     def get_action_list(self):
+        if self.action_list is None:
+            return []
         return self.pre_actions + self.action_list + self.post_actions
 
     def get_all_targets(self):
         """
         result = SCons.Util.UniqueList([])
         for target in self.get_all_targets():
-            result.extend(target.prerequisites)
+            if target.prerequisites is not None:
+                result.extend(target.prerequisites)
         return result
 
     def get_action_side_effects(self):
     """A null Executor, with a null build Environment, that does
     nothing when the rest of the methods call it.
 
-    This might be able to disapper when we refactor things to
+    This might be able to disappear when we refactor things to
     disassociate Builders from Nodes entirely, so we're not
     going to worry about unit tests for this--at least for now.
     """
         self._morph()
         self.set_action_list(action)
 
-
 # Local Variables:
 # tab-width:4
 # indent-tabs-mode:nil

File src/engine/SCons/Node/FS.py

         self.scanner_paths = {}
         if not hasattr(self, '_local'):
             self._local = 0
+        if not hasattr(self, 'released_target_info'):
+            self.released_target_info = False
 
         # If there was already a Builder set on this entry, then
         # we need to make sure we call the target-decider function,
         return self.get_build_env().get_CacheDir().retrieve(self)
 
     def visited(self):
-        if self.exists():
+        if self.exists() and self.executor is not None:
             self.get_build_env().get_CacheDir().push_if_forced(self)
 
         ninfo = self.get_ninfo()
 
         self.store_info()
 
+    def release_target_info(self):
+        """Called just after this node has been marked
+         up-to-date or was built completely.
+         
+         This is where we try to release as many target node infos
+         as possible for clean builds and update runs, in order
+         to minimize the overall memory consumption.
+         
+         We'd like to remove a lot more attributes like self.sources
+         and self.sources_set, but they might get used
+         in a next build step. For example, during configuration
+         the source files for a built *.o file are used to figure out
+         which linker to use for the resulting Program (gcc vs. g++)!
+         That's why we check for the 'keep_targetinfo' attribute,
+         config Nodes and the Interactive mode just don't allow
+         an early release of most variables. 
+
+         In the same manner, we can't simply remove the self.attributes
+         here. The smart linking relies on the shared flag, and some
+         parts of the java Tool use it to transport information
+         about nodes...
+         
+         @see: built() and Node.release_target_info()
+         """
+        if (self.released_target_info or SCons.Node.interactive):
+            return
+        
+        if not hasattr(self.attributes, 'keep_targetinfo'):
+            # Cache some required values, before releasing
+            # stuff like env, executor and builder...
+            self.changed()
+            self.get_contents_sig()
+            self.get_build_env()
+            # Now purge unneeded stuff to free memory...
+            self.executor = None
+            self._memo.pop('rfile', None)
+            self.prerequisites = None
+            # Cleanup lists, but only if they're empty
+            if not len(self.ignore_set):
+                self.ignore_set = None
+            if not len(self.implicit_set):
+                self.implicit_set = None
+            if not len(self.depends_set):
+                self.depends_set = None
+            if not len(self.ignore):
+                self.ignore = None
+            if not len(self.depends):
+                self.depends = None
+            # Mark this node as done, we only have to release
+            # the memory once...
+            self.released_target_info = True
+
     def find_src_builder(self):
         if self.rexists():
             return None
         SCons.Node.Node.builder_set(self, builder)
         self.changed_since_last_build = self.decide_target
 
+    def built(self):
+        """Called just after this File node is successfully built.
+        
+         Just like for 'release_target_info' we try to release
+         some more target node attributes in order to minimize the
+         overall memory consumption.
+         
+         @see: release_target_info
+        """
+
+        SCons.Node.Node.built(self)
+
+        if not hasattr(self.attributes, 'keep_targetinfo'):
+            # Ensure that the build infos get computed and cached...        
+            self.store_info()
+            # ... then release some more variables.
+            self._specific_sources = False
+            self.labspath = None
+            self._save_str()
+            self.cwd = None
+             
+            self.scanner_paths = None
+
+    def changed(self, node=None):
+        """
+        Returns if the node is up-to-date with respect to the BuildInfo
+        stored last time it was built. 
+        
+        For File nodes this is basically a wrapper around Node.changed(),
+        but we allow the return value to get cached after the reference
+        to the Executor got released in release_target_info().
+        """
+        if node is None:
+            try:
+                return self._memo['changed']
+            except KeyError:
+                pass
+        
+        has_changed = SCons.Node.Node.changed(self, node)
+        self._memo['changed'] = has_changed
+        return has_changed
+
     def changed_content(self, target, prev_ni):
         cur_csig = self.get_csig()
         try:
             self.cachedir_csig = self.get_csig()
         return self.cachedir_csig
 
+    def get_contents_sig(self):
+        """
+        A helper method for get_cachedir_bsig.
+
+        It computes and returns the signature for this
+        node's contents.
+        """
+        
+        try:
+            return self.contentsig
+        except AttributeError:
+            pass
+        
+        executor = self.get_executor()
+
+        result = self.contentsig = SCons.Util.MD5signature(executor.get_contents())
+        return result
+
     def get_cachedir_bsig(self):
+        """
+        Return the signature for a cached file, including
+        its children.
+
+        It adds the path of the cached file to the cache signature,
+        because multiple targets built by the same action will all
+        have the same build signature, and we have to differentiate
+        them somehow.
+        """
         try:
             return self.cachesig
         except AttributeError:
             pass
-
-        # Add the path to the cache signature, because multiple
-        # targets built by the same action will all have the same
-        # build signature, and we have to differentiate them somehow.
+        
+        # Collect signatures for all children
         children = self.children()
-        executor = self.get_executor()
-        # sigs = [n.get_cachedir_csig() for n in children]
         sigs = [n.get_cachedir_csig() for n in children]
-        sigs.append(SCons.Util.MD5signature(executor.get_contents()))
+        # Append this node's signature...
+        sigs.append(self.get_contents_sig())
+        # ...and it's path
         sigs.append(self.path)
+        # Merge this all into a single signature
         result = self.cachesig = SCons.Util.MD5collect(sigs)
         return result
 
-
 default_fs = None
 
 def get_default_fs():

File src/engine/SCons/Node/__init__.py

 
 Annotate = do_nothing
 
+# Gets set to 'True' if we're running in interactive mode. Is
+# currently used to release parts of a target's info during
+# clean builds and update runs (see release_target_info).
+interactive = False
+
 # Classes for signature info for Nodes.
 
 class NodeInfoBase(object):
         self.depends_set = set()
         self.ignore = []        # dependencies to ignore
         self.ignore_set = set()
-        self.prerequisites = SCons.Util.UniqueList()
+        self.prerequisites = None
         self.implicit = None    # implicit (scanned) dependencies (None means not scanned yet)
         self.waiting_parents = set()
         self.waiting_s_e = set()
         except AttributeError:
             pass
         else:
-            executor.cleanup()
+            if executor is not None:
+                executor.cleanup()
 
     def reset_executor(self):
         "Remove cached executor; forces recompute when needed."
         methods should call this base class method to get the child
         check and the BuildInfo structure.
         """
-        for d in self.depends:
-            if d.missing():
-                msg = "Explicit dependency `%s' not found, needed by target `%s'."
-                raise SCons.Errors.StopError(msg % (d, self))
+        if self.depends is not None:
+            for d in self.depends:
+                if d.missing():
+                    msg = "Explicit dependency `%s' not found, needed by target `%s'."
+                    raise SCons.Errors.StopError(msg % (d, self))
         if self.implicit is not None:
             for i in self.implicit:
                 if i.missing():
             self.ninfo.update(self)
             self.store_info()
 
+    def release_target_info(self):
+        """Called just after this node has been marked
+         up-to-date or was built completely.
+         
+         This is where we try to release as many target node infos
+         as possible for clean builds and update runs, in order
+         to minimize the overall memory consumption.
+         
+         By purging attributes that aren't needed any longer after
+         a Node (=File) got built, we don't have to care that much how
+         many KBytes a Node actually requires...as long as we free
+         the memory shortly afterwards.
+         
+         @see: built() and File.release_target_info()
+         """
+        pass
+
     #
     #
     #
 
     def is_derived(self):
         """
-        Returns true iff this node is derived (i.e. built).
+        Returns true if this node is derived (i.e. built).
 
         This should return true only for nodes whose path should be in
         the variant directory when duplicate=0 and should contribute their build
 
     def add_prerequisite(self, prerequisite):
         """Adds prerequisites"""
+        if self.prerequisites is None:
+            self.prerequisites = SCons.Util.UniqueList()
         self.prerequisites.extend(prerequisite)
         self._children_reset()
 
         # dictionary patterns I found all ended up using "not in"
         # internally anyway...)
         if self.ignore_set:
-            if self.implicit is None:
-                iter = chain(self.sources,self.depends)
-            else:
-                iter = chain(self.sources, self.depends, self.implicit)
+            iter = chain.from_iterable(filter(None, [self.sources, self.depends, self.implicit]))
 
             children = []
             for i in iter:
                 if i not in self.ignore_set:
                     children.append(i)
         else:
-            if self.implicit is None:
-                children = self.sources + self.depends
-            else:
-                children = self.sources + self.depends + self.implicit
+            children = self.all_children(scan=0)
 
         self._memo['children_get'] = children
         return children
         # using dictionary keys, lose the order, and the only ordered
         # dictionary patterns I found all ended up using "not in"
         # internally anyway...)
-        if self.implicit is None:
-            return self.sources + self.depends
-        else:
-            return self.sources + self.depends + self.implicit
+        return list(chain.from_iterable(filter(None, [self.sources, self.depends, self.implicit])))
 
     def children(self, scan=1):
         """Return a list of the node's direct children, minus those
         Return a text representation, suitable for displaying to the
         user, of the include tree for the sources of this node.
         """
-        if self.is_derived() and self.env:
+        if self.is_derived():
             env = self.get_build_env()
-            for s in self.sources:
-                scanner = self.get_source_scanner(s)
-                if scanner:
-                    path = self.get_build_scanner_path(scanner)
-                else:
-                    path = None
-                def f(node, env=env, scanner=scanner, path=path):
-                    return node.get_found_includes(env, scanner, path)
-                return SCons.Util.render_tree(s, f, 1)
+            if env:
+                for s in self.sources:
+                    scanner = self.get_source_scanner(s)
+                    if scanner:
+                        path = self.get_build_scanner_path(scanner)
+                    else:
+                        path = None
+                    def f(node, env=env, scanner=scanner, path=path):
+                        return node.get_found_includes(env, scanner, path)
+                    return SCons.Util.render_tree(s, f, 1)
         else:
             return None
 

File src/engine/SCons/SConf.py

         # so we really control how it gets written.
         for n in nodes:
             n.store_info = n.do_not_store_info
+            if not hasattr(n, 'attributes'):
+                n.attributes = SCons.Node.Node.Attrs()
+            n.attributes.keep_targetinfo = 1
 
         ret = 1
 

File src/engine/SCons/SConfTests.py

                         self.waiting_parents = set()
                         self.side_effects = []
                         self.builder = None
-                        self.prerequisites = []
+                        self.prerequisites = None
                     def disambiguate(self):
                         return self
                     def has_builder(self):

File src/engine/SCons/Script/Main.py

     platform = SCons.Platform.platform_module()
 
     if options.interactive:
+        SCons.Node.interactive = True
         SCons.Script.Interactive.interact(fs, OptionsParser, options,
                                           targets, target_top)
 

File src/engine/SCons/Taskmaster.py

         # or implicit dependencies exists, and also initialize the
         # .sconsign info.
         executor = self.targets[0].get_executor()
+        if executor is None:
+            return
         executor.prepare()
         for t in executor.get_action_targets():
             if print_prepare:
         post-visit actions that must take place regardless of whether
         or not the target was an actual built target or a source Node.
         """
+        global print_prepare
         T = self.tm.trace
         if T: T.write(self.trace_message('Task.executed_with_callbacks()',
                                          self.node))
                 if not t.cached:
                     t.push_to_cache()
                 t.built()
-            t.visited()
+                t.visited()
+                if (not print_prepare and 
+                    (not hasattr(self, 'options') or not self.options.debug_includes)):
+                    t.release_target_info()
+            else:
+                t.visited()
 
     executed = executed_with_callbacks
 
 
         This is the default behavior for building only what's necessary.
         """
+        global print_prepare
         T = self.tm.trace
         if T: T.write(self.trace_message(u'Task.make_ready_current()',
                                          self.node))
                 # parallel build...)
                 t.visited()
                 t.set_state(NODE_UP_TO_DATE)
+                if (not print_prepare and 
+                    (not hasattr(self, 'options') or not self.options.debug_includes)):
+                    t.release_target_info()
 
     make_ready = make_ready_current
 
                 parents[p] = parents.get(p, 0) + 1
 
         for t in targets:
-            for s in t.side_effects:
-                if s.get_state() == NODE_EXECUTING:
-                    s.set_state(NODE_NO_STATE)
-                    for p in s.waiting_parents:
-                        parents[p] = parents.get(p, 0) + 1
-                for p in s.waiting_s_e:
-                    if p.ref_count == 0:
-                        self.tm.candidates.append(p)
+            if t.side_effects is not None:
+                for s in t.side_effects:
+                    if s.get_state() == NODE_EXECUTING:
+                        s.set_state(NODE_NO_STATE)
+                        for p in s.waiting_parents:
+                            parents[p] = parents.get(p, 0) + 1
+                    for p in s.waiting_s_e:
+                        if p.ref_count == 0:
+                            self.tm.candidates.append(p)
 
         for p, subtract in parents.items():
             p.ref_count = p.ref_count - subtract
         if node is None:
             return None
 
-        tlist = node.get_executor().get_all_targets()
+        executor = node.get_executor()
+        if executor is None:
+            return None
+        
+        tlist = executor.get_all_targets()
 
         task = self.tasker(self, tlist, node in self.original_top, node)
         try:

File src/engine/SCons/TaskmasterTests.py

         self.scanned = 0
         self.scanner = None
         self.targets = [self]
-        self.prerequisites = []
+        self.prerequisites = None
         class Builder(object):
             def targets(self, node):
                 return node.targets
 
         self.clear()
         
+    def release_target_info(self):
+        pass
+
     def has_builder(self):
         return not self.builder is None