def execute_graph(self, requested_nodes=None):
        """Travel the graph, executing each node.

        Works by executing node by node until all of them
        are finished.

        Once a node is finished, it's outputs are sent to
        the inputs of other linked nodes, if any.
        """
        ### if there's no nodes in the graph, notify user
        ### via dialog and cancel operation by returning
        ### earlier

        if not self.nodes:

            create_and_show_dialog(AT_LEAST_ONE_TO_EXECUTE, level_name='info')
            return

        ### define the nodes to be visited by filtering out the commented out
        ### ones

        ## reference and clear set to hold references of nodes to be visited

        nodes_to_visit = self.nodes_to_visit
        nodes_to_visit.clear()

        ## if the nodes requested for execution were not specified...

        if requested_nodes is None:

            ## consider all existing ones as requested
            requested_nodes = self.nodes

            ## reference message to show in case requested nodes are commented out
            commented_out_message = ALL_WERE_COMMENTED_OUT

        ## if the nodes to be visited were specified, reference message to show in
        ## case requested nodes are commented out

        else:
            commented_out_message = ONLY_GIVEN_WERE_COMMENTED_OUT

        ## filter out commented out nodes

        nodes_to_visit.update(
            node
            for node in requested_nodes
            if not node.data.get('commented_out', False)
        )

        ## if no nodes to visit remain, notify user via dialog and cancel
        ## operation by returning earlier

        if not nodes_to_visit:

            create_and_show_dialog(commented_out_message, level_name='info')
            return

        ### let's also split the nodes to visit into different categories
        ### by storing them in dedicated sets
        ###
        ### the nodes meant for sorting are also subject to additional
        ### setups and checks

        ## clear sets

        self.clear_redirect_nodes()
        self.clear_data_nodes()
        self.clear_callable_mode_nodes()
        self.clear_nodes_to_sort()

        ## reference their add() operations locally for quicker and easier
        ## access

        add_redirect_node = self.add_redirect_node
        add_data_node = self.add_data_node
        add_callable_mode_node = self.add_callable_mode_node
        add_node_to_sort = self.add_node_to_sort

        ## try iterating over nodes, adding them to specific sets and
        ## performing extra tasks as needed

        try:

            for node in nodes_to_visit:

                if not hasattr(node, 'main_callable'):

                    if 'source_name' in node.data:
                        add_redirect_node(node)

                    else:
                        add_data_node(node)

                elif node.data.get('mode') == 'callable':
                    add_callable_mode_node(node)

                else:

                    add_node_to_sort(node)

                    ## perform extra setups/checks needed to ensure the
                    ## node is able and ready to be executed after being
                    ## sorted

                    node.perform_execution_setup()
                    node.check_and_setup_parameters()

        ## if one of the nodes meant for sorting is lacking input(s),
        ## notify user via dialog and cancel operation by returning earlier

        except LackOfInputError as err:

            create_and_show_dialog(str(err), level_name='error')
            return

        ### perform checks and redirect data from the corresponding
        ### set of nodes

        try:
            _check_data_nodes_and_send_data(self.data_nodes)

        except ProxyNodesLackingDataError as err:

            create_and_show_dialog(str(err), level_name='error')
            return

        ### reference callables from the corresponding set of nodes
        _reference_callables(self.callable_mode_nodes)

        ### at this point, all the nodes, except the ones for sorting,
        ### were visited and dealt with already;
        ###
        ### now it is time to sort the remaining nodes so we can execute
        ### them
        self._sort_nodes()

        ### make preparations to execute the sorted nodes

        ## reference and clear list to store references of executed nodes

        executed_nodes = self.executed_nodes
        executed_nodes.clear()

        ## reference and clear map to store execution time
        ## of individual nodes

        node_exec_time_map = self.node_exec_time_map
        node_exec_time_map.clear()

        ## mark the beginning of the node layout execution
        layout_exec_start = time()


        ### we can finally start executing the sorted nodes;
        ###
        ### the nodes were sorted and divided in different groups
        ### called generations;
        ###
        ### iterate over the sorted nodes one by one, executing them and
        ### performing several other related tasks as needed, like sending
        ### output to the next nodes, etc.

        for node in chain.from_iterable(self.node_generations):

            ### try executing the node

            try:

                ## first, perform pre-execution setups
                node.perform_pre_execution_setups()

                ## check whether node has a callable used as a
                ## backdoor to retrieve visuals from it, storing
                ## such backdoor if so

                for var_name in BACKDOOR_INDICATIVE_VAR_NAMES:

                    if hasattr(node, var_name):
                        backdoor = getattr(node, var_name)
                        break

                else:
                    backdoor = None

                ## pick appropriate callable object depending on
                ## whether the backdoor exists
                callable_obj = backdoor if backdoor else node.main_callable

                ### try executing the callable by passing the needed
                ### arguments to a function that will execute it and
                ### return the callable's return value

                try:

                    ## execute

                    node_exec_start = time()

                    return_value = lay_arguments_and_execute(
                        callable_obj, node.argument_map, node.signature_obj
                    )

                    node_exec_time_map[node.id] = time() - node_exec_start

                    output_to_send = return_value

                ### if an unexpected error occurs,
                ### raise a custom error from the
                ### original one

                except Exception as err:
                    raise NodeCallableError(node) from err

                ### if needed:
                ###   - redefine output to be sent to downstream nodes
                ###   - set visuals/looping

                try:

                    if backdoor:

                        node.set_visual(return_value['in_graph_visual'])

                        if hasattr(node, 'loop_entering_command'):

                            node.loop_data = return_value['loop_data']

                            if node.preview_toolbar.check_button.get():
                                node.loop_entering_command()

                        output_to_send = return_value.get('output')

                    elif hasattr(node, SIDEVIZ_FROM_OUTPUT_VAR_NAME):

                        node.set_visual(
                            node.get_sideviz_from_output(return_value)
                        )

                        ###

                        loop_data_retrieval_op = (
                            getattr(node, LOOPVIZ_FROM_OUTPUT_VAR_NAME, None)
                        )

                        if loop_data_retrieval_op:

                            node.loop_data = loop_data_retrieval_op(return_value)

                            if node.preview_toolbar.check_button.get():
                                node.loop_entering_command()

                except Exception as err:

                    raise RuntimeError(
                        "Error while setting visual/looping/output."
                    ) from err


                ###

                # send its return value to
                # other nodes as needed

                _send_output_to_connected_nodes(
                    node,
                    output_to_send,
                )

                # perform its execution setup
                node.perform_execution_setup()

                # append node to list of executed ones
                executed_nodes.append(node)


            ### if an error is thrown, act according to its
            ### class

            except Exception as err:

                ## clear all stored arguments
                _clear_arguments(self.nodes)

                ## if the error is among the ones listed
                ## below, just notify the user via dialog,
                ## using the error converted to a string
                ## as the error message

                if isinstance(
                    err,
                    (
                        UnexpectedOutputError,
                        PositionalSubparameterUnpackingError,
                        KeywordSubparameterUnpackingError,
                    ),
                ):

                    create_and_show_dialog(str(err), level_name="error")

                ## any other kind of error is either within the node being
                ## executed or completely unexpected, so we take further
                ## measures

                else:


                    ## build a custom log message to be logged
                    ## depending on whether the error was caused
                    ## during call/execution of a node's callable
                    ## or not

                    if isinstance(err, NodeCallableError):

                        # grab the node wherein the
                        # error bubbled up
                        error_node = err.node

                        # grab the original error
                        original_error = err.__cause__

                        log_message = (
                            f"'{error_node.title_text}'() callable from node"
                            f" #{error_node.id} raised an error."
                        )


                    ## otherwise we just notify the user
                    ## with a custom error message

                    else:

                        log_message = (
                            "An unexpected error occurred during graph"
                            " execution."
                        )


                    ## log traceback

                    logger.exception(log_message)
                    USER_LOGGER.exception(log_message)

                    ## notify user via dialog

                    dialog_message = log_message + (
                        " Check the user log for more info (on"
                        " graph/canvas, press <Ctrl+Shift+j> or access"
                        " the \"Help > Show user log\" option on menubar)."
                    )

                    create_and_show_dialog(dialog_message, level_name='error')


                ## break out of the for-loop (stop iterating over sorted
                ## nodes for execution)
                break

        ### provided everything went ok, we didn't have to break out of the
        ### for-loop, which means we now enter this corresponding else clause

        else:

            ### clear all stored arguments on all nodes
            _clear_arguments(self.nodes)

            ### report the time taken to execute the layout in the
            ### status label

            ## XXX both layout execution time and indivual
            ## node time (as well as their sum) should also
            ## be logged in the user logger

            layout_exec_time = time() - layout_exec_start

            tracked_nodes_total = sum(

                ## item
                node_exec_time_map[node.id]

                ## source
                for node in executed_nodes

                ## filter
                if not getattr(
                    node.signature_callable, 'dismiss_exec_time_tracking', False
                )
            )

            time_for_humans = friendly_delta_from_secs(tracked_nodes_total)

            set_status_message(f'Total execution time was {time_for_humans}')
Travel the graph, executing each node.

        Works by executing node by node until all of them
        are finished.

        Once a node is finished, it's outputs are sent to
        the inputs of other linked nodes, if any.
        
nodezator/graphman/execution.py