Beispiel #1
0
 def clear_graph(self):
     self.prev_graph_id = ()
     self.graphw = CGraphPlain(self.cfg.suite)
     self.normal_fit = True
     self.update_xdot()
     # gtk idle functions must return false or will be called multiple times
     return False
Beispiel #2
0
 def clear_graph(self):
     self.prev_graph_id = ()
     self.graphw = CGraphPlain(self.cfg.suite)
     self.normal_fit = True
     self.update_xdot()
     # gtk idle functions must return false or will be called multiple times
     return False
Beispiel #3
0
    def get_graph(self):
        title = self.suite + ': runtime inheritance graph'
        graph = CGraphPlain(title)
        graph.set_def_style(
            gtk_rgb_to_hex(getattr(self.style, 'fg', None)[gtk.STATE_NORMAL]),
            gtk_rgb_to_hex(getattr(self.style, 'bg', None)[gtk.STATE_NORMAL]))
        graph.graph_attr['rankdir'] = self.orientation
        for ns in self.inherit:
            for p in self.inherit[ns]:
                graph.add_edge(p, ns)
                graph.get_node(p).attr['shape'] = 'box'
                graph.get_node(ns).attr['shape'] = 'box'

        self.graph = graph
        self.filter_graph()
        self.set_dotcode(graph.string())
Beispiel #4
0
    def get_graph(self):
        title = self.suite + ': runtime inheritance graph'
        graph = CGraphPlain(title)
        graph.graph_attr['rankdir'] = self.orientation
        for ns in self.inherit:
            for p in self.inherit[ns]:
                attr = {}
                attr['color'] = 'royalblue'
                graph.add_edge(p, ns, **attr)
                nl = graph.get_node(p)
                nr = graph.get_node(ns)
                for n in nl, nr:
                    n.attr['shape'] = 'box'
                    n.attr['style'] = 'filled'
                    n.attr['fillcolor'] = 'powderblue'
                    n.attr['color'] = 'royalblue'

        self.graph = graph
        self.filter_graph()
        self.set_dotcode(graph.string())
Beispiel #5
0
    def get_graph(self):
        title = self.suite + ': runtime inheritance graph'
        graph = CGraphPlain(title)
        graph.graph_attr['rankdir'] = self.orientation
        for ns in self.inherit:
            for p in self.inherit[ns]:
                attr = {}
                attr['color'] = 'royalblue'
                graph.add_edge(p, ns, **attr)
                nl = graph.get_node(p)
                nr = graph.get_node(ns)
                for n in nl, nr:
                    n.attr['shape'] = 'box'
                    n.attr['style'] = 'filled'
                    n.attr['fillcolor'] = 'powderblue'
                    n.attr['color'] = 'royalblue'

        self.graph = graph
        self.filter_graph()
        self.set_dotcode(graph.string())
Beispiel #6
0
    def __init__(self, cfg, updater, theme, info_bar, xdot):
        super(GraphUpdater, self).__init__()

        self.quit = False
        self.cleared = False
        self.ignore_suicide = True
        self.focus_start_point_string = None
        self.focus_stop_point_string = None
        self.xdot = xdot
        self.first_update = False
        self.graph_disconnect = False
        self.action_required = True
        self.oldest_point_string = None
        self.newest_point_string = None
        self.orientation = "TB"  # Top to Bottom ordering of nodes
        self.best_fit = True  # zoom to page size
        self.normal_fit = False  # zoom to 1.0 scale
        self.crop = False
        self.subgraphs_on = False  # organise by cycle point.

        self.descendants = {}
        self.all_families = []
        self.write_dot_frames = False

        self.prev_graph_id = ()

        self.cfg = cfg
        self.updater = updater
        self.theme = theme
        self.info_bar = info_bar
        self.state_summary = {}
        self.fam_state_summary = {}
        self.global_summary = {}
        self.last_update_time = None

        self.god = None
        self.mode = "waiting..."
        self.update_time_str = "waiting..."

        self.prev_graph_id = ()

        # empty graphw object:
        self.graphw = CGraphPlain(self.cfg.suite)

        # TODO - handle failure to get a remote proxy in reconnect()

        self.graph_warned = {}

        # lists of nodes to newly group or ungroup (not of all currently
        # grouped and ungrouped nodes - still held server side)
        self.group = []
        self.ungroup = []
        self.have_leaves_and_feet = False
        self.leaves = []
        self.feet = []

        self.ungroup_recursive = False
        if "graph" in self.cfg.ungrouped_views:
            self.ungroup_all = True
            self.group_all = False
        else:
            self.ungroup_all = False
            self.group_all = True

        self.graph_frame_count = 0
        self.suite_share_dir = GLOBAL_CFG.get_derived_host_item(
            self.cfg.suite, 'suite share directory')
Beispiel #7
0
class GraphUpdater(threading.Thread):
    def __init__(self, cfg, updater, theme, info_bar, xdot):
        super(GraphUpdater, self).__init__()

        self.quit = False
        self.cleared = False
        self.ignore_suicide = True
        self.focus_start_point_string = None
        self.focus_stop_point_string = None
        self.xdot = xdot
        self.first_update = False
        self.graph_disconnect = False
        self.action_required = True
        self.oldest_point_string = None
        self.newest_point_string = None
        self.orientation = "TB"  # Top to Bottom ordering of nodes
        self.best_fit = True  # zoom to page size
        self.normal_fit = False  # zoom to 1.0 scale
        self.crop = False
        self.subgraphs_on = False  # organise by cycle point.

        self.descendants = {}
        self.all_families = []
        self.write_dot_frames = False

        self.prev_graph_id = ()

        self.cfg = cfg
        self.updater = updater
        self.theme = theme
        self.info_bar = info_bar
        self.state_summary = {}
        self.fam_state_summary = {}
        self.global_summary = {}
        self.last_update_time = None

        self.god = None
        self.mode = "waiting..."
        self.update_time_str = "waiting..."

        self.prev_graph_id = ()

        # empty graphw object:
        self.graphw = CGraphPlain(self.cfg.suite)

        # TODO - handle failure to get a remote proxy in reconnect()

        self.graph_warned = {}

        # lists of nodes to newly group or ungroup (not of all currently
        # grouped and ungrouped nodes - still held server side)
        self.group = []
        self.ungroup = []
        self.have_leaves_and_feet = False
        self.leaves = []
        self.feet = []

        self.ungroup_recursive = False
        if "graph" in self.cfg.ungrouped_views:
            self.ungroup_all = True
            self.group_all = False
        else:
            self.ungroup_all = False
            self.group_all = True

        self.graph_frame_count = 0
        self.suite_share_dir = GLOBAL_CFG.get_derived_host_item(
            self.cfg.suite, 'suite share directory')

    def toggle_write_dot_frames(self):
        self.write_dot_frames = not self.write_dot_frames
        if self.write_dot_frames:
            # Create local share dir if necessary (could be a remote suite).
            try:
                mkdir_p(self.suite_share_dir)
            except Exception as exc:
                gobject.idle_add(
                    warning_dialog(
                        "%s\nCannot create graph frames directory." %
                        (str(exc))).warn)
                self.write_dot_frames = False

    def clear_graph(self):
        self.prev_graph_id = ()
        self.graphw = CGraphPlain(self.cfg.suite)
        self.normal_fit = True
        self.update_xdot()
        # gtk idle functions must return false or will be called multiple times
        return False

    def get_summary(self, task_id):
        return get_id_summary(task_id, self.state_summary,
                              self.fam_state_summary, self.descendants)

    def update(self):
        if not self.updater.connected:
            if not self.cleared:
                gobject.idle_add(self.clear_graph)
                self.cleared = True
            return False
        self.cleared = False

        if (self.last_update_time is not None
                and self.last_update_time >= self.updater.last_update_time):
            if self.action_required:
                return True
            return False

        self.updater.set_update(False)
        states_full = deepcopy(self.updater.state_summary)
        fam_states_full = deepcopy(self.updater.fam_state_summary)
        self.ancestors = deepcopy(self.updater.ancestors)
        self.descendants = deepcopy(self.updater.descendants)
        self.all_families = deepcopy(self.updater.all_families)
        self.global_summary = deepcopy(self.updater.global_summary)
        self.updater.set_update(True)

        self.first_update = (self.last_update_time is None)
        self.last_update_time = self.updater.last_update_time

        # The graph layout is not stable even when (py)graphviz is
        # presented with the same graph (may be a node ordering issue
        # due to use of dicts?). For this reason we only plot node name
        # and color (state) and only replot when node content or states
        # change.  The full state summary contains task timing
        # information that changes continually, so we have to disregard
        # this when checking for changes. So: just extract the critical
        # info here:
        states = {}
        for id_, state_full in states_full.items():
            if id_ not in states:
                states[id_] = {}
            for key in [
                    'name', 'description', 'title', 'label', 'state',
                    'submit_num'
            ]:
                if key in state_full:  # ensure backward compatible
                    states[id_][key] = state_full[key]

        f_states = {}
        for id_, fam_state_full in fam_states_full.items():
            if id_ not in states:
                f_states[id_] = {}
            for key in ['name', 'description', 'title', 'label', 'state']:
                if key in fam_state_full:  # ensure backward compatible
                    f_states[id_][key] = fam_state_full[key]

        if states and not self.state_summary:
            # This is basically equivalent to a first-update case.
            self.first_update = True

        # only update states if a change occurred, or action required
        if self.action_required:
            self.state_summary = states
            self.fam_state_summary = f_states
            return True
        elif self.graph_disconnect:
            return False
        elif not compare_dict_of_dict(states, self.state_summary):
            # state changed - implicitly includes family state change.
            self.state_summary = states
            self.fam_state_summary = f_states
            return True
        else:
            return False

    def run(self):
        glbl = None
        while not self.quit:
            if self.update():
                if self.global_summary:
                    needed_no_redraw = self.update_graph()
                    # DO NOT USE gobject.idle_add() HERE - IT DRASTICALLY
                    # AFFECTS PERFORMANCE FOR LARGE SUITES? appears to
                    # be unnecessary anyway (due to xdot internals?)
                    self.update_xdot(no_zoom=needed_no_redraw)
            sleep(0.2)
        else:
            pass

    def update_xdot(self, no_zoom=False):
        self.xdot.set_dotcode(self.graphw.to_string(), no_zoom=True)
        if self.first_update:
            self.xdot.widget.zoom_to_fit()
            self.first_update = False
        elif self.best_fit:
            self.xdot.widget.zoom_to_fit()
            self.best_fit = False
        elif self.normal_fit:
            self.xdot.widget.zoom_image(1.0, center=True)
            self.normal_fit = False

    def set_live_node_attr(self, node, id, shape=None):
        # override base graph URL to distinguish live tasks
        node.attr['URL'] = id
        if id in self.state_summary:
            state = self.state_summary[id]['state']
        else:
            state = self.fam_state_summary[id]['state']

        try:
            node.attr['style'] = 'bold,' + self.theme[state]['style']
            node.attr['fillcolor'] = self.theme[state]['color']
            node.attr['color'] = self.theme[state]['color']
            node.attr['fontcolor'] = self.theme[state]['fontcolor']
        except KeyError:
            # unknown state
            node.attr['style'] = 'unfilled'
            node.attr['color'] = 'black'
            node.attr['fontcolor'] = 'black'

        if shape:
            node.attr['shape'] = shape

    def update_graph(self):
        # TODO - check edges against resolved ones
        # (adding new ones, and nodes, if necessary)

        self.action_required = False
        try:
            self.oldest_point_string = (
                self.global_summary['oldest cycle point string'])
            self.newest_point_string = (
                self.global_summary['newest cycle point string'])
            if TASK_STATUS_RUNAHEAD not in self.updater.filter_states_excl:
                # Get a graph out to the max runahead point.
                try:
                    self.newest_point_string = (
                        self.
                        global_summary['newest runahead cycle point string'])
                except KeyError:
                    # back compat <= 6.2.0
                    pass
        except KeyError:
            # Pre cylc-6 back compat.
            self.oldest_point_string = (
                self.global_summary['oldest cycle time'])
            self.newest_point_string = (
                self.global_summary['newest cycle time'])

        if self.focus_start_point_string:
            oldest = self.focus_start_point_string
            newest = self.focus_stop_point_string
        else:
            oldest = self.oldest_point_string
            newest = self.newest_point_string

        group_for_server = self.group
        if self.group == []:
            group_for_server = None

        ungroup_for_server = self.ungroup
        if self.ungroup == []:
            ungroup_for_server = None

        try:
            res = self.updater.suite_info_client.get_info(
                'get_graph_raw',
                start_point_string=oldest,
                stop_point_string=newest,
                group_nodes=group_for_server,
                ungroup_nodes=ungroup_for_server,
                ungroup_recursive=self.ungroup_recursive,
                group_all=self.group_all,
                ungroup_all=self.ungroup_all)
        except Exception as exc:
            print >> sys.stderr, str(exc)
            return False

        self.have_leaves_and_feet = True
        gr_edges, suite_polling_tasks, self.leaves, self.feet = res
        gr_edges = [tuple(edge) for edge in gr_edges]

        current_id = self.get_graph_id(gr_edges)
        needs_redraw = current_id != self.prev_graph_id

        if needs_redraw:
            self.graphw = CGraphPlain(self.cfg.suite, suite_polling_tasks)
            self.graphw.add_edges(gr_edges, ignore_suicide=self.ignore_suicide)

            nodes_to_remove = set()

            # Remove nodes representing filtered-out tasks.
            if (self.updater.filter_name_string
                    or self.updater.filter_states_excl):
                for node in self.graphw.nodes():
                    id = node.get_name()
                    # Don't need to guard against special nodes here (yet).
                    name, point_string = TaskID.split(id)
                    if name not in self.all_families:
                        # This node is a task, not a family.
                        if id in self.updater.filt_task_ids:
                            nodes_to_remove.add(node)
                        elif id not in self.updater.kept_task_ids:
                            # A base node - these only appear in the graph.
                            filter_string = self.updater.filter_name_string
                            if (filter_string and filter_string not in name
                                    and not re.search(filter_string, name)):
                                # A base node that fails the name filter.
                                nodes_to_remove.add(node)
                    elif id in self.fam_state_summary:
                        # Remove family nodes if all members filtered out.
                        remove = True
                        for mem in self.descendants[name]:
                            mem_id = TaskID.get(mem, point_string)
                            if mem_id in self.updater.kept_task_ids:
                                remove = False
                                break
                        if remove:
                            nodes_to_remove.add(node)
                    elif id in self.updater.full_fam_state_summary:
                        # An updater-filtered-out family.
                        nodes_to_remove.add(node)

            # Base node cropping.
            if self.crop:
                # Remove all base nodes.
                for node in (set(self.graphw.nodes()) - nodes_to_remove):
                    if node.get_name() not in self.state_summary:
                        nodes_to_remove.add(node)
            else:
                # Remove cycle points containing only base nodes.
                non_base_point_strings = set()
                point_string_nodes = {}
                for node in set(self.graphw.nodes()) - nodes_to_remove:
                    node_id = node.get_name()
                    name, point_string = TaskID.split(node_id)
                    point_string_nodes.setdefault(point_string, [])
                    point_string_nodes[point_string].append(node)
                    if (node_id in self.state_summary
                            or node_id in self.fam_state_summary):
                        non_base_point_strings.add(point_string)
                pure_base_point_strings = (set(point_string_nodes) -
                                           non_base_point_strings)
                for point_string in pure_base_point_strings:
                    for node in point_string_nodes[point_string]:
                        nodes_to_remove.add(node)
            self.graphw.cylc_remove_nodes_from(list(nodes_to_remove))
            # TODO - remove base nodes only connected to other base nodes?
            # Should these even exist any more?

            # Make family nodes octagons.
            for node in self.graphw.nodes():
                node_id = node.get_name()
                try:
                    name, point_string = TaskID.split(node_id)
                except ValueError:
                    # Special node.
                    continue
                if name in self.all_families:
                    node.attr['shape'] = 'doubleoctagon'

            if self.subgraphs_on:
                self.graphw.add_cycle_point_subgraphs(gr_edges)

        # Set base node style defaults
        for node in self.graphw.nodes():
            node.attr.setdefault('style', 'filled')
            node.attr['color'] = '#888888'
            node.attr['fillcolor'] = 'white'
            node.attr['fontcolor'] = '#888888'

        for id in self.state_summary:
            try:
                node = self.graphw.get_node(id)
            except KeyError:
                continue
            self.set_live_node_attr(node, id)

        for id in self.fam_state_summary:
            try:
                node = self.graphw.get_node(id)
            except:
                continue
            self.set_live_node_attr(node, id)

        self.graphw.graph_attr['rankdir'] = self.orientation

        if self.write_dot_frames:
            arg = os.path.join(
                self.suite_share_dir,
                'frame' + '-' + str(self.graph_frame_count) + '.dot')
            self.graphw.write(arg)
            self.graph_frame_count += 1

        self.prev_graph_id = current_id
        return not needs_redraw

    def get_graph_id(self, edges):
        """If any of these quantities change, the graph should be redrawn."""
        node_ids = set()
        for edge in edges:
            node_ids.add(edge[0])
            node_ids.add(edge[1])
        # Get a set of ids that are actually present in the state summaries.
        # We need this in case of no-longer-purely-base-node cycle points.
        node_ids_in_state = set(node_ids).intersection(
            set(self.state_summary).union(set(self.fam_state_summary)))
        # Return a key that maps to the essential structure of the graph.
        return (set(edges), self.crop, node_ids_in_state,
                set(self.updater.filter_states_excl),
                self.updater.filter_name_string, self.orientation,
                self.ignore_suicide, self.subgraphs_on)
Beispiel #8
0
    def update_graph(self):
        # TODO - check edges against resolved ones
        # (adding new ones, and nodes, if necessary)

        self.action_required = False
        try:
            self.oldest_point_string = (
                self.global_summary['oldest cycle point string'])
            self.newest_point_string = (
                self.global_summary['newest cycle point string'])
            if TASK_STATUS_RUNAHEAD not in self.updater.filter_states_excl:
                # Get a graph out to the max runahead point.
                try:
                    self.newest_point_string = (
                        self.
                        global_summary['newest runahead cycle point string'])
                except KeyError:
                    # back compat <= 6.2.0
                    pass
        except KeyError:
            # Pre cylc-6 back compat.
            self.oldest_point_string = (
                self.global_summary['oldest cycle time'])
            self.newest_point_string = (
                self.global_summary['newest cycle time'])

        if self.focus_start_point_string:
            oldest = self.focus_start_point_string
            newest = self.focus_stop_point_string
        else:
            oldest = self.oldest_point_string
            newest = self.newest_point_string

        group_for_server = self.group
        if self.group == []:
            group_for_server = None

        ungroup_for_server = self.ungroup
        if self.ungroup == []:
            ungroup_for_server = None

        try:
            res = self.updater.suite_info_client.get_info(
                'get_graph_raw',
                start_point_string=oldest,
                stop_point_string=newest,
                group_nodes=group_for_server,
                ungroup_nodes=ungroup_for_server,
                ungroup_recursive=self.ungroup_recursive,
                group_all=self.group_all,
                ungroup_all=self.ungroup_all)
        except Exception as exc:
            print >> sys.stderr, str(exc)
            return False

        self.have_leaves_and_feet = True
        gr_edges, suite_polling_tasks, self.leaves, self.feet = res
        gr_edges = [tuple(edge) for edge in gr_edges]

        current_id = self.get_graph_id(gr_edges)
        needs_redraw = current_id != self.prev_graph_id

        if needs_redraw:
            self.graphw = CGraphPlain(self.cfg.suite, suite_polling_tasks)
            self.graphw.add_edges(gr_edges, ignore_suicide=self.ignore_suicide)

            nodes_to_remove = set()

            # Remove nodes representing filtered-out tasks.
            if (self.updater.filter_name_string
                    or self.updater.filter_states_excl):
                for node in self.graphw.nodes():
                    id = node.get_name()
                    # Don't need to guard against special nodes here (yet).
                    name, point_string = TaskID.split(id)
                    if name not in self.all_families:
                        # This node is a task, not a family.
                        if id in self.updater.filt_task_ids:
                            nodes_to_remove.add(node)
                        elif id not in self.updater.kept_task_ids:
                            # A base node - these only appear in the graph.
                            filter_string = self.updater.filter_name_string
                            if (filter_string and filter_string not in name
                                    and not re.search(filter_string, name)):
                                # A base node that fails the name filter.
                                nodes_to_remove.add(node)
                    elif id in self.fam_state_summary:
                        # Remove family nodes if all members filtered out.
                        remove = True
                        for mem in self.descendants[name]:
                            mem_id = TaskID.get(mem, point_string)
                            if mem_id in self.updater.kept_task_ids:
                                remove = False
                                break
                        if remove:
                            nodes_to_remove.add(node)
                    elif id in self.updater.full_fam_state_summary:
                        # An updater-filtered-out family.
                        nodes_to_remove.add(node)

            # Base node cropping.
            if self.crop:
                # Remove all base nodes.
                for node in (set(self.graphw.nodes()) - nodes_to_remove):
                    if node.get_name() not in self.state_summary:
                        nodes_to_remove.add(node)
            else:
                # Remove cycle points containing only base nodes.
                non_base_point_strings = set()
                point_string_nodes = {}
                for node in set(self.graphw.nodes()) - nodes_to_remove:
                    node_id = node.get_name()
                    name, point_string = TaskID.split(node_id)
                    point_string_nodes.setdefault(point_string, [])
                    point_string_nodes[point_string].append(node)
                    if (node_id in self.state_summary
                            or node_id in self.fam_state_summary):
                        non_base_point_strings.add(point_string)
                pure_base_point_strings = (set(point_string_nodes) -
                                           non_base_point_strings)
                for point_string in pure_base_point_strings:
                    for node in point_string_nodes[point_string]:
                        nodes_to_remove.add(node)
            self.graphw.cylc_remove_nodes_from(list(nodes_to_remove))
            # TODO - remove base nodes only connected to other base nodes?
            # Should these even exist any more?

            # Make family nodes octagons.
            for node in self.graphw.nodes():
                node_id = node.get_name()
                try:
                    name, point_string = TaskID.split(node_id)
                except ValueError:
                    # Special node.
                    continue
                if name in self.all_families:
                    node.attr['shape'] = 'doubleoctagon'

            if self.subgraphs_on:
                self.graphw.add_cycle_point_subgraphs(gr_edges)

        # Set base node style defaults
        for node in self.graphw.nodes():
            node.attr.setdefault('style', 'filled')
            node.attr['color'] = '#888888'
            node.attr['fillcolor'] = 'white'
            node.attr['fontcolor'] = '#888888'

        for id in self.state_summary:
            try:
                node = self.graphw.get_node(id)
            except KeyError:
                continue
            self.set_live_node_attr(node, id)

        for id in self.fam_state_summary:
            try:
                node = self.graphw.get_node(id)
            except:
                continue
            self.set_live_node_attr(node, id)

        self.graphw.graph_attr['rankdir'] = self.orientation

        if self.write_dot_frames:
            arg = os.path.join(
                self.suite_share_dir,
                'frame' + '-' + str(self.graph_frame_count) + '.dot')
            self.graphw.write(arg)
            self.graph_frame_count += 1

        self.prev_graph_id = current_id
        return not needs_redraw
Beispiel #9
0
    def __init__(self, cfg, updater, theme, info_bar, xdot):
        super(GraphUpdater, self).__init__()

        self.quit = False
        self.cleared = False
        self.ignore_suicide = True
        self.focus_start_point_string = None
        self.focus_stop_point_string = None
        self.xdot = xdot
        self.first_update = False
        self.graph_disconnect = False
        self.action_required = True
        self.oldest_point_string = None
        self.newest_point_string = None
        self.orientation = "TB"  # Top to Bottom ordering of nodes
        self.best_fit = True  # zoom to page size
        self.normal_fit = False  # zoom to 1.0 scale
        self.crop = False
        self.subgraphs_on = False   # organise by cycle point.

        self.descendants = {}
        self.all_families = []
        self.write_dot_frames = False

        self.prev_graph_id = ()

        self.cfg = cfg
        self.updater = updater
        self.theme = theme
        self.info_bar = info_bar
        self.state_summary = {}
        self.fam_state_summary = {}
        self.global_summary = {}
        self.last_update_time = None

        self.god = None
        self.mode = "waiting..."
        self.update_time_str = "waiting..."

        self.prev_graph_id = ()

        # empty graphw object:
        self.graphw = CGraphPlain(self.cfg.suite)

        # lists of nodes to newly group or ungroup (not of all currently
        # grouped and ungrouped nodes - still held server side)
        self.group = []
        self.ungroup = []
        self.have_leaves_and_feet = False
        self.leaves = []
        self.feet = []

        self.ungroup_recursive = False
        if "graph" in self.cfg.ungrouped_views:
            self.ungroup_all = True
            self.group_all = False
        else:
            self.ungroup_all = False
            self.group_all = True

        self.graph_frame_count = 0
        self.suite_share_dir = glbl_cfg().get_derived_host_item(
            self.cfg.suite, 'suite share directory')
Beispiel #10
0
class GraphUpdater(threading.Thread):
    """Thread for updating "cylc gui" graph view."""

    PREFIX_BASE = 'base:'

    def __init__(self, cfg, updater, theme, info_bar, xdot):
        super(GraphUpdater, self).__init__()

        self.quit = False
        self.cleared = False
        self.ignore_suicide = True
        self.focus_start_point_string = None
        self.focus_stop_point_string = None
        self.xdot = xdot
        self.first_update = False
        self.graph_disconnect = False
        self.action_required = True
        self.oldest_point_string = None
        self.newest_point_string = None
        self.orientation = "TB"  # Top to Bottom ordering of nodes
        self.best_fit = True  # zoom to page size
        self.normal_fit = False  # zoom to 1.0 scale
        self.crop = False
        self.subgraphs_on = False   # organise by cycle point.

        self.descendants = {}
        self.all_families = []
        self.write_dot_frames = False

        self.prev_graph_id = ()

        self.cfg = cfg
        self.updater = updater
        self.theme = theme
        self.info_bar = info_bar
        self.state_summary = {}
        self.fam_state_summary = {}
        self.global_summary = {}
        self.last_update_time = None

        self.god = None
        self.mode = "waiting..."
        self.update_time_str = "waiting..."

        self.prev_graph_id = ()

        # empty graphw object:
        self.graphw = CGraphPlain(self.cfg.suite)

        # lists of nodes to newly group or ungroup (not of all currently
        # grouped and ungrouped nodes - still held server side)
        self.group = []
        self.ungroup = []
        self.have_leaves_and_feet = False
        self.leaves = []
        self.feet = []

        self.ungroup_recursive = False
        if "graph" in self.cfg.ungrouped_views:
            self.ungroup_all = True
            self.group_all = False
        else:
            self.ungroup_all = False
            self.group_all = True

        self.graph_frame_count = 0
        self.suite_share_dir = glbl_cfg().get_derived_host_item(
            self.cfg.suite, 'suite share directory')

    def toggle_write_dot_frames(self):
        self.write_dot_frames = not self.write_dot_frames
        if self.write_dot_frames:
            # Create local share dir if necessary (could be a remote suite).
            try:
                mkdir_p(self.suite_share_dir)
            except Exception as exc:
                gobject.idle_add(warning_dialog(
                    "%s\nCannot create graph frames directory." % (str(exc))
                ).warn)
                self.write_dot_frames = False

    def clear_gui(self):
        """Clear the graph GUI."""
        self.prev_graph_id = ()
        self.graphw = CGraphPlain(self.cfg.suite)
        self.normal_fit = True
        self.update_xdot()
        # gtk idle functions must return false or will be called multiple times
        return False

    def get_summary(self, task_id):
        """Return some state information about a task or family."""
        return get_id_summary(
            task_id, self.state_summary, self.fam_state_summary,
            self.descendants)

    def update(self):
        """Update data using data from self.updater."""
        if not self.updater.connected:
            if not self.cleared:
                gobject.idle_add(self.clear_gui)
                self.cleared = True
            return False
        self.cleared = False

        if (self.last_update_time is not None and
                self.last_update_time >= self.updater.last_update_time):
            if self.action_required:
                return True
            return False

        self.updater.no_update_event.set()
        states_full = deepcopy(self.updater.state_summary)
        fam_states_full = deepcopy(self.updater.fam_state_summary)
        self.ancestors = deepcopy(self.updater.ancestors)
        self.descendants = deepcopy(self.updater.descendants)
        self.all_families = deepcopy(self.updater.all_families)
        self.global_summary = deepcopy(self.updater.global_summary)
        self.updater.no_update_event.clear()

        self.first_update = (self.last_update_time is None)
        self.last_update_time = self.updater.last_update_time

        # The graph layout is not stable even when (py)graphviz is
        # presented with the same graph (may be a node ordering issue
        # due to use of dicts?). For this reason we only plot node name
        # and color (state) and only replot when node content or states
        # change.  The full state summary contains task timing
        # information that changes continually, so we have to disregard
        # this when checking for changes. So: just extract the critical
        # info here:
        states = {}
        for id_, state_full in states_full.items():
            if id_ not in states:
                states[id_] = {}
            for key in [
                    'name', 'description', 'title', 'label', 'state',
                    'submit_num']:
                if key in state_full:  # ensure backward compatible
                    states[id_][key] = state_full[key]

        f_states = {}
        for id_, fam_state_full in fam_states_full.items():
            if id_ not in states:
                f_states[id_] = {}
            for key in ['name', 'description', 'title', 'label', 'state']:
                if key in fam_state_full:  # ensure backward compatible
                    f_states[id_][key] = fam_state_full[key]

        if states and not self.state_summary:
            # This is basically equivalent to a first-update case.
            self.first_update = True

        # only update states if a change occurred, or action required
        if self.action_required:
            self.state_summary = states
            self.fam_state_summary = f_states
            return True
        elif self.graph_disconnect:
            return False
        elif not compare_dict_of_dict(states, self.state_summary):
            # state changed - implicitly includes family state change.
            self.state_summary = states
            self.fam_state_summary = f_states
            return True
        else:
            return False

    def run(self):
        while not self.quit:
            if self.update():
                self.update_gui()
            sleep(0.2)

    def update_xdot(self, no_zoom=False):
        self.xdot.set_dotcode(self.graphw.to_string(), no_zoom=no_zoom)
        if self.first_update:
            self.xdot.widget.zoom_to_fit()
            self.first_update = False
        elif self.best_fit:
            self.xdot.widget.zoom_to_fit()
            self.best_fit = False
        elif self.normal_fit:
            self.xdot.widget.zoom_image(1.0, center=True)
            self.normal_fit = False

    def set_live_node_attr(self, node, id_, shape=None):
        # override base graph URL to distinguish live tasks
        node.attr['URL'] = id_
        if id_ in self.state_summary:
            state = self.state_summary[id_]['state']
        else:
            state = self.fam_state_summary[id_]['state']

        try:
            node.attr['style'] = 'bold,' + self.theme[state]['style']
            node.attr['fillcolor'] = self.theme[state]['color']
            node.attr['color'] = self.theme[state]['color']
            node.attr['fontcolor'] = self.theme[state]['fontcolor']
        except KeyError:
            # unknown state
            node.attr['style'] = 'unfilled'
            node.attr['color'] = 'black'
            node.attr['fontcolor'] = 'black'

        if shape:
            node.attr['shape'] = shape

    def update_gui(self):
        # TODO - check edges against resolved ones
        # (adding new ones, and nodes, if necessary)
        self.action_required = False
        if not self.global_summary:
            return

        self.oldest_point_string = (
            self.global_summary['oldest cycle point string'])
        self.newest_point_string = (
            self.global_summary['newest cycle point string'])
        if TASK_STATUS_RUNAHEAD not in self.updater.filter_states_excl:
            # Get a graph out to the max runahead point.
            self.newest_point_string = (
                self.global_summary[
                    'newest runahead cycle point string'])

        if self.focus_start_point_string:
            oldest = self.focus_start_point_string
            newest = self.focus_stop_point_string
        else:
            oldest = self.oldest_point_string
            newest = self.newest_point_string

        group_for_server = self.group
        if self.group == []:
            group_for_server = None

        ungroup_for_server = self.ungroup
        if self.ungroup == []:
            ungroup_for_server = None

        try:
            res = self.updater.client.get_info(
                'get_graph_raw', start_point_string=oldest,
                stop_point_string=newest,
                group_nodes=group_for_server,
                ungroup_nodes=ungroup_for_server,
                ungroup_recursive=self.ungroup_recursive,
                group_all=self.group_all,
                ungroup_all=self.ungroup_all
            )
        except ClientError:
            if cylc.flags.debug:
                try:
                    traceback.print_exc()
                except IOError:
                    pass  # Cannot print to terminal (session may be closed).

            return False

        self.have_leaves_and_feet = True
        gr_edges, suite_polling_tasks, self.leaves, self.feet = res
        gr_edges = [tuple(edge) for edge in gr_edges]

        current_id = self.get_graph_id(gr_edges)
        if current_id != self.prev_graph_id:
            self.graphw = CGraphPlain(
                self.cfg.suite, suite_polling_tasks)
            self.graphw.add_edges(
                gr_edges, ignore_suicide=self.ignore_suicide)

            nodes_to_remove = set()

            # Remove nodes representing filtered-out tasks.
            if (self.updater.filter_name_string or
                    self.updater.filter_states_excl):
                for node in self.graphw.nodes():
                    id_ = node.get_name()
                    # Don't need to guard against special nodes here (yet).
                    name, point_string = TaskID.split(id_)
                    if name not in self.all_families:
                        # This node is a task, not a family.
                        if id_ in self.updater.filt_task_ids:
                            nodes_to_remove.add(node)
                        elif id_ not in self.updater.kept_task_ids:
                            # A base node - these only appear in the graph.
                            filter_string = self.updater.filter_name_string
                            if (filter_string and
                                    filter_string not in name and
                                    not re.search(filter_string, name)):
                                # A base node that fails the name filter.
                                nodes_to_remove.add(node)
                    elif id_ in self.fam_state_summary:
                        # Remove family nodes if all members filtered out.
                        remove = True
                        for mem in self.descendants[name]:
                            mem_id = TaskID.get(mem, point_string)
                            if mem_id in self.updater.kept_task_ids:
                                remove = False
                                break
                        if remove:
                            nodes_to_remove.add(node)
                    elif id_ in self.updater.full_fam_state_summary:
                        # An updater-filtered-out family.
                        nodes_to_remove.add(node)

            # Base node cropping.
            if self.crop:
                # Remove all base nodes.
                for node in (set(self.graphw.nodes()) - nodes_to_remove):
                    if node.get_name() not in self.state_summary:
                        nodes_to_remove.add(node)
            else:
                # Remove cycle points containing only base nodes.
                non_base_point_strings = set()
                point_string_nodes = {}
                for node in set(self.graphw.nodes()) - nodes_to_remove:
                    node_id = node.get_name()
                    name, point_string = TaskID.split(node_id)
                    point_string_nodes.setdefault(point_string, [])
                    point_string_nodes[point_string].append(node)
                    if (node_id in self.state_summary or
                            node_id in self.fam_state_summary):
                        non_base_point_strings.add(point_string)
                pure_base_point_strings = (
                    set(point_string_nodes) - non_base_point_strings)
                for point_string in pure_base_point_strings:
                    for node in point_string_nodes[point_string]:
                        nodes_to_remove.add(node)
            self.graphw.cylc_remove_nodes_from(list(nodes_to_remove))
            # TODO - remove base nodes only connected to other base nodes?
            # Should these even exist any more?

            # Make family nodes octagons.
            for node in self.graphw.nodes():
                node_id = node.get_name()
                try:
                    name, point_string = TaskID.split(node_id)
                except ValueError:
                    # Special node.
                    continue
                if name in self.all_families:
                    node.attr['shape'] = 'doubleoctagon'

            if self.subgraphs_on:
                self.graphw.add_cycle_point_subgraphs(gr_edges)

        # Set base node style defaults
        for node in self.graphw.nodes():
            node.attr.setdefault('style', 'filled')
            node.attr['color'] = '#888888'
            node.attr['fillcolor'] = 'white'
            node.attr['fontcolor'] = '#888888'
            if not node.attr['URL'].startswith(self.PREFIX_BASE):
                node.attr['URL'] = self.PREFIX_BASE + node.attr['URL']

        for id_ in self.state_summary:
            try:
                node = self.graphw.get_node(id_)
            except KeyError:
                continue
            self.set_live_node_attr(node, id_)

        for id_ in self.fam_state_summary:
            try:
                node = self.graphw.get_node(id_)
            except KeyError:
                # Node not in graph.
                continue
            self.set_live_node_attr(node, id_)

        self.graphw.graph_attr['rankdir'] = self.orientation

        if self.write_dot_frames:
            arg = os.path.join(
                self.suite_share_dir, 'frame' + '-' +
                str(self.graph_frame_count) + '.dot')
            self.graphw.write(arg)
            self.graph_frame_count += 1

        self.update_xdot(no_zoom=(current_id == self.prev_graph_id))
        self.prev_graph_id = current_id

    def get_graph_id(self, edges):
        """If any of these quantities change, the graph should be redrawn."""
        node_ids = set()
        for edge in edges:
            node_ids.add(edge[0])
            node_ids.add(edge[1])
        # Get a set of ids that are actually present in the state summaries.
        # We need this in case of no-longer-purely-base-node cycle points.
        node_ids_in_state = set(node_ids).intersection(
            set(self.state_summary).union(set(self.fam_state_summary)))
        # Return a key that maps to the essential structure of the graph.
        return (set(edges), self.crop, node_ids_in_state,
                set(self.updater.filter_states_excl),
                self.updater.filter_name_string,
                self.orientation, self.ignore_suicide, self.subgraphs_on)
Beispiel #11
0
    def update_gui(self):
        # TODO - check edges against resolved ones
        # (adding new ones, and nodes, if necessary)
        self.action_required = False
        if not self.global_summary:
            return

        self.oldest_point_string = (
            self.global_summary['oldest cycle point string'])
        self.newest_point_string = (
            self.global_summary['newest cycle point string'])
        if TASK_STATUS_RUNAHEAD not in self.updater.filter_states_excl:
            # Get a graph out to the max runahead point.
            self.newest_point_string = (
                self.global_summary[
                    'newest runahead cycle point string'])

        if self.focus_start_point_string:
            oldest = self.focus_start_point_string
            newest = self.focus_stop_point_string
        else:
            oldest = self.oldest_point_string
            newest = self.newest_point_string

        group_for_server = self.group
        if self.group == []:
            group_for_server = None

        ungroup_for_server = self.ungroup
        if self.ungroup == []:
            ungroup_for_server = None

        try:
            res = self.updater.client.get_info(
                'get_graph_raw', start_point_string=oldest,
                stop_point_string=newest,
                group_nodes=group_for_server,
                ungroup_nodes=ungroup_for_server,
                ungroup_recursive=self.ungroup_recursive,
                group_all=self.group_all,
                ungroup_all=self.ungroup_all
            )
        except ClientError:
            if cylc.flags.debug:
                try:
                    traceback.print_exc()
                except IOError:
                    pass  # Cannot print to terminal (session may be closed).

            return False

        self.have_leaves_and_feet = True
        gr_edges, suite_polling_tasks, self.leaves, self.feet = res
        gr_edges = [tuple(edge) for edge in gr_edges]

        current_id = self.get_graph_id(gr_edges)
        if current_id != self.prev_graph_id:
            self.graphw = CGraphPlain(
                self.cfg.suite, suite_polling_tasks)
            self.graphw.add_edges(
                gr_edges, ignore_suicide=self.ignore_suicide)

            nodes_to_remove = set()

            # Remove nodes representing filtered-out tasks.
            if (self.updater.filter_name_string or
                    self.updater.filter_states_excl):
                for node in self.graphw.nodes():
                    id_ = node.get_name()
                    # Don't need to guard against special nodes here (yet).
                    name, point_string = TaskID.split(id_)
                    if name not in self.all_families:
                        # This node is a task, not a family.
                        if id_ in self.updater.filt_task_ids:
                            nodes_to_remove.add(node)
                        elif id_ not in self.updater.kept_task_ids:
                            # A base node - these only appear in the graph.
                            filter_string = self.updater.filter_name_string
                            if (filter_string and
                                    filter_string not in name and
                                    not re.search(filter_string, name)):
                                # A base node that fails the name filter.
                                nodes_to_remove.add(node)
                    elif id_ in self.fam_state_summary:
                        # Remove family nodes if all members filtered out.
                        remove = True
                        for mem in self.descendants[name]:
                            mem_id = TaskID.get(mem, point_string)
                            if mem_id in self.updater.kept_task_ids:
                                remove = False
                                break
                        if remove:
                            nodes_to_remove.add(node)
                    elif id_ in self.updater.full_fam_state_summary:
                        # An updater-filtered-out family.
                        nodes_to_remove.add(node)

            # Base node cropping.
            if self.crop:
                # Remove all base nodes.
                for node in (set(self.graphw.nodes()) - nodes_to_remove):
                    if node.get_name() not in self.state_summary:
                        nodes_to_remove.add(node)
            else:
                # Remove cycle points containing only base nodes.
                non_base_point_strings = set()
                point_string_nodes = {}
                for node in set(self.graphw.nodes()) - nodes_to_remove:
                    node_id = node.get_name()
                    name, point_string = TaskID.split(node_id)
                    point_string_nodes.setdefault(point_string, [])
                    point_string_nodes[point_string].append(node)
                    if (node_id in self.state_summary or
                            node_id in self.fam_state_summary):
                        non_base_point_strings.add(point_string)
                pure_base_point_strings = (
                    set(point_string_nodes) - non_base_point_strings)
                for point_string in pure_base_point_strings:
                    for node in point_string_nodes[point_string]:
                        nodes_to_remove.add(node)
            self.graphw.cylc_remove_nodes_from(list(nodes_to_remove))
            # TODO - remove base nodes only connected to other base nodes?
            # Should these even exist any more?

            # Make family nodes octagons.
            for node in self.graphw.nodes():
                node_id = node.get_name()
                try:
                    name, point_string = TaskID.split(node_id)
                except ValueError:
                    # Special node.
                    continue
                if name in self.all_families:
                    node.attr['shape'] = 'doubleoctagon'

            if self.subgraphs_on:
                self.graphw.add_cycle_point_subgraphs(gr_edges)

        # Set base node style defaults
        for node in self.graphw.nodes():
            node.attr.setdefault('style', 'filled')
            node.attr['color'] = '#888888'
            node.attr['fillcolor'] = 'white'
            node.attr['fontcolor'] = '#888888'
            if not node.attr['URL'].startswith(self.PREFIX_BASE):
                node.attr['URL'] = self.PREFIX_BASE + node.attr['URL']

        for id_ in self.state_summary:
            try:
                node = self.graphw.get_node(id_)
            except KeyError:
                continue
            self.set_live_node_attr(node, id_)

        for id_ in self.fam_state_summary:
            try:
                node = self.graphw.get_node(id_)
            except KeyError:
                # Node not in graph.
                continue
            self.set_live_node_attr(node, id_)

        self.graphw.graph_attr['rankdir'] = self.orientation

        if self.write_dot_frames:
            arg = os.path.join(
                self.suite_share_dir, 'frame' + '-' +
                str(self.graph_frame_count) + '.dot')
            self.graphw.write(arg)
            self.graph_frame_count += 1

        self.update_xdot(no_zoom=(current_id == self.prev_graph_id))
        self.prev_graph_id = current_id
Beispiel #12
0
    def update_graph(self):
        # TODO - check edges against resolved ones
        # (adding new ones, and nodes, if necessary)

        self.action_required = False
        try:
            self.oldest_point_string = (
                self.global_summary['oldest cycle point string'])
            self.newest_point_string = (
                self.global_summary['newest cycle point string'])
            if TASK_STATUS_RUNAHEAD not in self.updater.filter_states_excl:
                # Get a graph out to the max runahead point.
                try:
                    self.newest_point_string = (
                        self.global_summary[
                            'newest runahead cycle point string'])
                except KeyError:
                    # back compat <= 6.2.0
                    pass
        except KeyError:
            # Pre cylc-6 back compat.
            self.oldest_point_string = (
                self.global_summary['oldest cycle time'])
            self.newest_point_string = (
                self.global_summary['newest cycle time'])

        if self.focus_start_point_string:
            oldest = self.focus_start_point_string
            newest = self.focus_stop_point_string
        else:
            oldest = self.oldest_point_string
            newest = self.newest_point_string

        try:
            res = self.updater.suite_info_client.get_info(
                'get_graph_raw', oldest, newest, self.group, self.ungroup,
                self.ungroup_recursive, self.group_all, self.ungroup_all)
        except TypeError:
            # Back compat with pre cylc-6 suite daemons.
            res = self.updater.suite_info_client.get(
                'get_graph_raw', oldest, newest, False, self.group,
                self.ungroup, self.ungroup_recursive, self.group_all,
                self.ungroup_all)
        except Exception as exc:  # PyroError?
            print >> sys.stderr, str(exc)
            return False

        # backward compatibility for old suite daemons still running
        self.have_leaves_and_feet = False
        if isinstance(res, list):
            # prior to suite-polling tasks in 5.4.0
            gr_edges = res
            suite_polling_tasks = []
            self.leaves = []
            self.feet = []
        else:
            if len(res) == 2:
                # prior to graph view grouping fix in 5.4.2
                gr_edges, suite_polling_tasks = res
                self.leaves = []
                self.feet = []
            elif len(res) == 4:
                # 5.4.2 and later
                self.have_leaves_and_feet = True
                gr_edges, suite_polling_tasks, self.leaves, self.feet = res

        current_id = self.get_graph_id(gr_edges)
        needs_redraw = current_id != self.prev_graph_id

        if needs_redraw:
            self.graphw = CGraphPlain(
                self.cfg.suite, suite_polling_tasks)
            self.graphw.add_edges(
                gr_edges, ignore_suicide=self.ignore_suicide)

            nodes_to_remove = set()

            # Remove nodes representing filtered-out tasks.
            if (self.updater.filter_name_string or
                    self.updater.filter_states_excl):
                for node in self.graphw.nodes():
                    id = node.get_name()
                    # Don't need to guard against special nodes here (yet).
                    name, point_string = TaskID.split(id)
                    if name not in self.all_families:
                        # This node is a task, not a family.
                        if id in self.updater.filt_task_ids:
                            nodes_to_remove.add(node)
                        elif id not in self.updater.kept_task_ids:
                            # A base node - these only appear in the graph.
                            filter_string = self.updater.filter_name_string
                            if (filter_string and
                                    filter_string not in name and
                                    not re.search(filter_string, name)):
                                # A base node that fails the name filter.
                                nodes_to_remove.add(node)
                    elif id in self.fam_state_summary:
                        # Remove family nodes if all members filtered out.
                        remove = True
                        for mem in self.descendants[name]:
                            mem_id = TaskID.get(mem, point_string)
                            if mem_id in self.updater.kept_task_ids:
                                remove = False
                                break
                        if remove:
                            nodes_to_remove.add(node)
                    elif id in self.updater.full_fam_state_summary:
                        # An updater-filtered-out family.
                        nodes_to_remove.add(node)

            # Base node cropping.
            if self.crop:
                # Remove all base nodes.
                for node in (set(self.graphw.nodes()) - nodes_to_remove):
                    if node.get_name() not in self.state_summary:
                        nodes_to_remove.add(node)
            else:
                # Remove cycle points containing only base nodes.
                non_base_point_strings = set()
                point_string_nodes = {}
                for node in set(self.graphw.nodes()) - nodes_to_remove:
                    node_id = node.get_name()
                    name, point_string = TaskID.split(node_id)
                    point_string_nodes.setdefault(point_string, [])
                    point_string_nodes[point_string].append(node)
                    if (node_id in self.state_summary or
                            node_id in self.fam_state_summary):
                        non_base_point_strings.add(point_string)
                pure_base_point_strings = (
                    set(point_string_nodes) - non_base_point_strings)
                for point_string in pure_base_point_strings:
                    for node in point_string_nodes[point_string]:
                        nodes_to_remove.add(node)
            self.graphw.cylc_remove_nodes_from(list(nodes_to_remove))
            # TODO - remove base nodes only connected to other base nodes?
            # Should these even exist any more?

            # Make family nodes octagons.
            for node in self.graphw.nodes():
                node_id = node.get_name()
                try:
                    name, point_string = TaskID.split(node_id)
                except ValueError:
                    # Special node.
                    continue
                if name in self.all_families:
                    if name in self.triggering_families:
                        node.attr['shape'] = 'doubleoctagon'
                    else:
                        node.attr['shape'] = 'tripleoctagon'

            if self.subgraphs_on:
                self.graphw.add_cycle_point_subgraphs(gr_edges)

        # Set base node style defaults
        for node in self.graphw.nodes():
            node.attr.setdefault('style', 'filled')
            node.attr['color'] = '#888888'
            node.attr['fillcolor'] = 'white'
            node.attr['fontcolor'] = '#888888'

        for id in self.state_summary:
            try:
                node = self.graphw.get_node(id)
            except KeyError:
                continue
            self.set_live_node_attr(node, id)

        for id in self.fam_state_summary:
            try:
                node = self.graphw.get_node(id)
            except:
                continue
            self.set_live_node_attr(node, id)

        self.graphw.graph_attr['rankdir'] = self.orientation

        if self.write_dot_frames:
            arg = os.path.join(
                self.suite_share_dir, 'frame' + '-' +
                str(self.graph_frame_count) + '.dot')
            self.graphw.write(arg)
            self.graph_frame_count += 1

        self.prev_graph_id = current_id
        return not needs_redraw
Beispiel #13
0
    def update_gui(self):
        # TODO - check edges against resolved ones
        # (adding new ones, and nodes, if necessary)
        self.action_required = False
        if not self.global_summary:
            return

        self.oldest_point_string = (
            self.global_summary['oldest cycle point string'])
        self.newest_point_string = (
            self.global_summary['newest cycle point string'])
        if TASK_STATUS_RUNAHEAD not in self.updater.filter_states_excl:
            # Get a graph out to the max runahead point.
            self.newest_point_string = (
                self.global_summary['newest runahead cycle point string'])

        if self.focus_start_point_string:
            oldest = self.focus_start_point_string
            newest = self.focus_stop_point_string
        else:
            oldest = self.oldest_point_string
            newest = self.newest_point_string

        group_for_server = self.group
        if self.group == []:
            group_for_server = None

        ungroup_for_server = self.ungroup
        if self.ungroup == []:
            ungroup_for_server = None

        try:
            res = self.updater.client.get_info(
                'get_graph_raw',
                start_point_string=oldest,
                stop_point_string=newest,
                group_nodes=group_for_server,
                ungroup_nodes=ungroup_for_server,
                ungroup_recursive=self.ungroup_recursive,
                group_all=self.group_all,
                ungroup_all=self.ungroup_all)
        except ClientError:
            if cylc.flags.debug:
                try:
                    traceback.print_exc()
                except IOError:
                    pass  # Cannot print to terminal (session may be closed).

            return False

        self.have_leaves_and_feet = True
        gr_edges, suite_polling_tasks, self.leaves, self.feet = res
        gr_edges = [tuple(edge) for edge in gr_edges]
        fgcolor = gtk_rgb_to_hex(
            getattr(self.xdot.widget.style, 'fg', None)[gtk.STATE_NORMAL])

        current_id = self.get_graph_id(gr_edges)
        if current_id != self.prev_graph_id:
            self.graphw = CGraphPlain(self.cfg.suite, suite_polling_tasks)
            self.graphw.add_edges(gr_edges, ignore_suicide=self.ignore_suicide)

            nodes_to_remove = set()

            # Remove nodes representing filtered-out tasks.
            if (self.updater.filter_name_string
                    or self.updater.filter_states_excl):
                for node in self.graphw.nodes():
                    id_ = node.get_name()
                    # Don't need to guard against special nodes here (yet).
                    name, point_string = TaskID.split(id_)
                    if name not in self.all_families:
                        # This node is a task, not a family.
                        if id_ in self.updater.filt_task_ids:
                            nodes_to_remove.add(node)
                        elif id_ not in self.updater.kept_task_ids:
                            # A base node - these only appear in the graph.
                            filter_string = self.updater.filter_name_string
                            if (filter_string and filter_string not in name
                                    and not re.search(filter_string, name)):
                                # A base node that fails the name filter.
                                nodes_to_remove.add(node)
                    elif id_ in self.fam_state_summary:
                        # Remove family nodes if all members filtered out.
                        remove = True
                        for mem in self.descendants[name]:
                            mem_id = TaskID.get(mem, point_string)
                            if mem_id in self.updater.kept_task_ids:
                                remove = False
                                break
                        if remove:
                            nodes_to_remove.add(node)
                    elif id_ in self.updater.full_fam_state_summary:
                        # An updater-filtered-out family.
                        nodes_to_remove.add(node)

            # Base node cropping.
            if self.crop:
                # Remove all base nodes.
                for node in (set(self.graphw.nodes()) - nodes_to_remove):
                    if node.get_name() not in self.state_summary:
                        nodes_to_remove.add(node)
            else:
                # Remove cycle points containing only base nodes.
                non_base_point_strings = set()
                point_string_nodes = {}
                for node in set(self.graphw.nodes()) - nodes_to_remove:
                    node_id = node.get_name()
                    name, point_string = TaskID.split(node_id)
                    point_string_nodes.setdefault(point_string, [])
                    point_string_nodes[point_string].append(node)
                    if (node_id in self.state_summary
                            or node_id in self.fam_state_summary):
                        non_base_point_strings.add(point_string)
                pure_base_point_strings = (set(point_string_nodes) -
                                           non_base_point_strings)
                for point_string in pure_base_point_strings:
                    for node in point_string_nodes[point_string]:
                        nodes_to_remove.add(node)
            self.graphw.cylc_remove_nodes_from(list(nodes_to_remove))
            # TODO - remove base nodes only connected to other base nodes?
            # Should these even exist any more?

            # Make family nodes octagons.
            for node in self.graphw.nodes():
                node_id = node.get_name()
                try:
                    name, point_string = TaskID.split(node_id)
                except ValueError:
                    # Special node.
                    continue
                if name in self.all_families:
                    node.attr['shape'] = 'doubleoctagon'
                elif name.startswith('@'):
                    node.attr['shape'] = 'none'

            if self.subgraphs_on:
                self.graphw.add_cycle_point_subgraphs(gr_edges, fgcolor)

        # Set base node style defaults
        fg_ghost = "%s%s" % (fgcolor, GHOST_TRANSP_HEX)
        for node in self.graphw.nodes():
            node.attr['style'] = 'dotted'
            node.attr['color'] = fg_ghost
            node.attr['fontcolor'] = fg_ghost
            if not node.attr['URL'].startswith(self.PREFIX_BASE):
                node.attr['URL'] = self.PREFIX_BASE + node.attr['URL']

        for id_ in self.state_summary:
            try:
                node = self.graphw.get_node(id_)
            except KeyError:
                continue
            self.set_live_node_attr(node, id_)

        for id_ in self.fam_state_summary:
            try:
                node = self.graphw.get_node(id_)
            except KeyError:
                # Node not in graph.
                continue
            self.set_live_node_attr(node, id_)

        self.graphw.graph_attr['rankdir'] = self.orientation

        if self.write_dot_frames:
            arg = os.path.join(
                self.suite_share_dir,
                'frame' + '-' + str(self.graph_frame_count) + '.dot')
            self.graphw.write(arg)
            self.graph_frame_count += 1

        self.update_xdot(no_zoom=(current_id == self.prev_graph_id))
        self.prev_graph_id = current_id