def render_frame(self, frame: Frame | None) -> list[SpeedscopeEvent]:
        """
        Builds up a list of speedscope events that are used to populate the
        "events" array in speedscope-formatted JSON.

        This method has two notable side effects:

        * it populates the self._frame_to_index dictionary that matches
          speedscope frames with their positions in the "shared" array of
          speedscope output; this dictionary will be used to write this
          "shared" array in the render method

        * it accumulates a running total of time elapsed by
          accumulating the self_time spent in each pyinstrument frame;
          this running total is used by speedscope events to construct
          a flame chart.
        """

        # if frame is None, recursion bottoms out; no event frames
        # need to be added
        if frame is None:
            return []

        # Otherwise, form a speedscope frame and add it to the frame
        # to index map if the frame is not already a key in that map.
        sframe = SpeedscopeFrame(frame.function, frame.file_path, frame.line_no)
        if sframe not in self._frame_to_index:
            self._frame_to_index[sframe] = len(self._frame_to_index)

        # Get the frame index and add a speedscope event corresponding
        # to opening a stack frame.
        sframe_index = self._frame_to_index[sframe]
        open_event = SpeedscopeEvent(SpeedscopeEventType.OPEN, self._event_time, sframe_index)
        events_array: list[SpeedscopeEvent] = [open_event]

        # Add stack frame open and close events for all child frames
        # of this frame.
        for child in frame.children:
            events_array.extend(self.render_frame(child))

        # Update event time for closing this stack frame.
        #
        # If number of frames approaches 1e16 * desired accuracy
        # level, consider using Neumaier-Kahan summation; improves
        # worst-case relative accuracy of sum from O(num_summands *
        # eps) to (2 * eps + O(num_summands * eps * eps)), where eps
        # is IEEE-754 double precision unit roundoff, approximately
        # 1e-16. Average case relative accuracy expressions replace
        # num_summands with sqrt(num_summands). However, Kahan
        # summation quadruples operation count of sum, and Neumaier
        # variant also adds a branch & swap for each summand. Pairwise
        # summation isn't an option here because a running total is
        # needed.

        self._event_time += frame.absorbed_time
        if frame.is_synthetic_leaf:
            # only time contained within leaf nodes is real time i.e. not the sum of children
            self._event_time += frame.time

        # Add event closing this stack frame.
        close_event = SpeedscopeEvent(SpeedscopeEventType.CLOSE, self._event_time, sframe_index)
        events_array.append(close_event)

        return events_array

        Builds up a list of speedscope events that are used to populate the
        "events" array in speedscope-formatted JSON.

        This method has two notable side effects:

        * it populates the self._frame_to_index dictionary that matches
          speedscope frames with their positions in the "shared" array of
          speedscope output; this dictionary will be used to write this
          "shared" array in the render method

        * it accumulates a running total of time elapsed by
          accumulating the self_time spent in each pyinstrument frame;
          this running total is used by speedscope events to construct
          a flame chart.
        
pyinstrument/renderers/speedscope.py