class_docstr: Object to store runtime and memory profiling information for all of
  PyTensor's operations: compilation, rewriting, execution.
  
  Parameters
  ----------
  atexit_print : bool
      True means that this object will be printed to stderr (using .summary())
      at the end of the program.
  **kwargs : misc initializers
      These should (but need not) match the names of the class vars declared
      in this class.
class_docstr_tok: ["Object","to","store","runtime","and","memory","profiling","information","for","all","of","PyTensor","'s","operations",":","compilation",",","rewriting",",","execution",".","Parameters","--","--","--","--","--","atexit_print",":","bool","True","means","that","this","object","will","be","printed","to","stderr","(","using",".summary","(",")",")","at","the","end","of","the","program",".","*","*","kwargs",":","misc","initializers","These","should","(","but","need","not",")","match","the","names","of","the","class","vars","declared","in","this","class","."]
code: def summary_nodes(self, file=sys.stderr, N=None):
      if self.apply_time:
          local_time = sum(self.apply_time.values())
      else:
          local_time = 0
      if local_time == 0:
          print(
              (
                  "ProfileStats.summary_nodes: total time 0"
                  " (did you forget to enable counters?)"
              ),
              file=file,
          )
          return
  
      print("Apply", file=file)
      print("------", file=file)
      # headers
      hs = []
      # formatting string
      es = []
  
      hs += ["<% time>"]
      es += ["  %4.1f%% "]
  
      hs += ["<sum %>"]
      es += [" %5.1f%% "]
  
      hs += ["<apply time>"]
      es += ["   %7.3fs "]
  
      hs += ["<time per call>"]
      es += ["     %8.2es "]
  
      hs += ["<#call>"]
      es += [" %4d  "]
  
      hs += ["<id>"]
      es += ["%3d"]
  
      es += ["%s", "%s"]
      if self.variable_shape:
          hs += ["<Mflops>", "<Gflops/s>"]
  
      upto_length = sum(len(x) for x in hs) + len(hs)
      maxlen = max(self.line_width - upto_length, 0)
      hs += ["<Apply name>"]
      es += ["%s"]
  
      header_str = " ".join(hs)
      format_str = " ".join(es)
  
      print(header_str, file=file)
  
      topos = {}  # Only do the topo once per fct.
      atimes = []
      for (fgraph, a), t in self.apply_time.items():
          if fgraph not in topos:
              topo = fgraph.toposort()
              topos[fgraph] = topo
          else:
              topo = topos[fgraph]
          atimes.append(
              (
                  t * 100 / local_time,
                  t,
                  a,
                  topo.index(a),
                  self.apply_callcount[(fgraph, a)],
              )
          )
      del topos
  
      atimes.sort(reverse=True, key=lambda t: (t[1], t[3]))
      tot = 0
      for f, t, a, nd_id, nb_call in atimes[:N]:
          tot += t
          ftot = tot * 100 / local_time
          if nb_call == 0:
              continue
          if not self.variable_shape:
              flops = ""
              flops_s = ""
          elif hasattr(a.op, "flops"):
              fl = a.op.flops(
                  [self.variable_shape[var] for var in a.inputs],
                  [self.variable_shape[var] for var in a.outputs],
              )
              flops = f"{fl / 1024.0 / 1024:8.1f}"
              flops_s = f"{fl / 1024.0 / 1024 / 1024 / t:10.1f}"
          else:
              flops = "        "
              flops_s = "          "
          print(
              format_str
              % (
                  f,
                  ftot,
                  t,
                  t / nb_call,
                  nb_call,
                  nd_id,
                  flops,
                  flops_s,
                  str(a)[:maxlen],
              ),
              file=file,
          )
          if not config.profile_memory:
              continue
          for idx, var in enumerate(a.inputs):
              sh = self.variable_shape.get(var, "no shape")
              st = self.variable_strides.get(var, "no strides")
              off = self.variable_offset.get(var, "")
              if off != "":
                  off = f", offset={off}"
              dtype = getattr(var, "dtype", "no dtype")
              print(
                  f"    input {int(idx)}: dtype={dtype}, shape={sh}, strides={st}{off}",
                  file=file,
              )
          for idx, var in enumerate(a.outputs):
              sh = self.variable_shape.get(var, "no shape")
              st = self.variable_strides.get(var, "no strides")
              off = self.variable_offset.get(var, "")
              if off != "":
                  off = f", offset={off}"
              dtype = getattr(var, "dtype", "no dtype")
              print(
                  f"    output {int(idx)}: dtype={dtype}, shape={sh}, strides={st}{off}",
                  file=file,
              )
          # Same as before, this I've sacrificed some information making
          # the output more readable
      print(
          "   ... (remaining %i Apply instances account for "
          "%.2f%%(%.2fs) of the runtime)"
          % (
              max(0, len(atimes) - N),
              sum(f for f, t, a, nd_id, nb_call in atimes[N:]),
              sum(t for f, t, a, nd_id, nb_call in atimes[N:]),
          ),
          file=file,
      )
      print("", file=file)
  
imports: import atexit
  import copy
  import logging
  import operator
  import sys
  import time
  from collections import defaultdict
  from contextlib import contextmanager
  from typing import TYPE_CHECKING, Any, Optional, Union
  import numpy
  import pytensor
  from pytensor.configdefaults import config
  from pytensor.graph.basic import Apply, Constant, Variable
  from pytensor.link.utils import get_destroy_dependencies
label_desc: Use image node_id 15 for calling the ProfileStats obj's underlying member method code with example usage: obj.summary_nodes(file, N) without return types