code: def compute_memory_contents_under_schedule(self, schedule):
      
      out_degree = self._compute_initial_out_degree()
  
      curr_memory_contents = set()
      memory_contents_for_each_operation = []
  
      for operation_id in schedule:
        operation_name = self._operations[operation_id].name
        # Allocate new memory to perform the computation at this node.
        for output_name in self.get_operation_output_names(operation_name):
          curr_memory_contents.add(output_name)
        memory_contents_for_each_operation.append(frozenset(curr_memory_contents))
  
        # Free any tensors which are no longer needed.
        for output_name in self.get_operation_output_names(operation_name):
          if out_degree[output_name] == 0:
            curr_memory_contents.remove(output_name)
        for input_name in self.get_operation_input_names(operation_name):
          out_degree[input_name] -= 1
          if out_degree[input_name] == 0:
            curr_memory_contents.remove(input_name)
  
      return memory_contents_for_each_operation
docs: The in-memory tensors present when executing each operation in schedule.
  
      Simulates running operations in the order given by a schedule. Keeps track
      of the tensors in memory at every point in time, and outputs a list (one
      entry for each point in time) of all sets of all memory contents (i.e. a
      frozenset of strings) ever seen in this execution.
  
      It is assumed (but not checked) that schedule is a valid topological sort of
      the operations in this graph.
  
      Args:
        schedule: A list of integer ids; the order to run operations in.
  
      Returns:
        a list of frozenset of strings, where the ith entry describes the tensors
        in memory when executing operation i (where schedule[i] is an index into
        get_all_operation_names()).