dvc: optimize building graph (#3443)

Use trie structure to efficiently check out, dep and stage paths being
prefixes of each other.
This commit is contained in:
Alexander Schepanovski
2020-03-09 12:13:07 +02:00
committed by GitHub
parent f05e6e0529
commit 357a6dd467
2 changed files with 42 additions and 32 deletions
+41 -32
View File
@@ -1,12 +1,11 @@
import os import os
from contextlib import contextmanager from contextlib import contextmanager
from functools import wraps from functools import wraps
from itertools import chain
from dvc.ignore import CleanTree from dvc.ignore import CleanTree
from dvc.compat import fspath_py35 from dvc.compat import fspath_py35
from funcy import cached_property, cat from funcy import cached_property, cat, first
from dvc.config import Config from dvc.config import Config
from dvc.exceptions import ( from dvc.exceptions import (
@@ -305,6 +304,7 @@ class Repo(object):
CyclicGraphError: resulting graph has cycles CyclicGraphError: resulting graph has cycles
""" """
import networkx as nx import networkx as nx
from pygtrie import Trie
from dvc.exceptions import ( from dvc.exceptions import (
OutputDuplicationError, OutputDuplicationError,
StagePathAsOutputError, StagePathAsOutputError,
@@ -314,48 +314,57 @@ class Repo(object):
G = nx.DiGraph() G = nx.DiGraph()
stages = stages or self.stages stages = stages or self.stages
stages = [stage for stage in stages if stage] stages = [stage for stage in stages if stage]
outs = {} outs = Trie() # Use trie to efficiently find overlapping outs and deps
for stage in stages: for stage in stages:
for out in stage.outs: for out in stage.outs:
if out.path_info in outs: out_key = out.path_info.parts
dup_stages = [stage, outs[out.path_info].stage]
# Check for dup outs
if out_key in outs:
dup_stages = [stage, outs[out_key].stage]
raise OutputDuplicationError(str(out), dup_stages) raise OutputDuplicationError(str(out), dup_stages)
outs[out.path_info] = out
# Check for overlapping outs
if outs.has_subtrie(out_key):
parent = out
overlapping = first(outs.values(prefix=out_key))
else:
parent = outs.shortest_prefix(out_key).value
overlapping = out
if parent and overlapping:
msg = (
"Paths for outs:\n'{}'('{}')\n'{}'('{}')\n"
"overlap. To avoid unpredictable behaviour, "
"rerun command with non overlapping outs paths."
).format(
str(parent),
parent.stage.relpath,
str(overlapping),
overlapping.stage.relpath,
)
raise OverlappingOutputPathsError(parent, overlapping, msg)
outs[out_key] = out
for stage in stages: for stage in stages:
for out in stage.outs: out = outs.shortest_prefix(PathInfo(stage.path).parts).value
for p in out.path_info.parents: if out:
if p in outs: raise StagePathAsOutputError(stage, str(out))
msg = (
"Paths for outs:\n'{}'('{}')\n'{}'('{}')\n"
"overlap. To avoid unpredictable behaviour, "
"rerun command with non overlapping outs paths."
).format(
str(outs[p]),
outs[p].stage.relpath,
str(out),
out.stage.relpath,
)
raise OverlappingOutputPathsError(outs[p], out, msg)
# Building graph
G.add_nodes_from(stages)
for stage in stages: for stage in stages:
stage_path_info = PathInfo(stage.path)
for p in chain([stage_path_info], stage_path_info.parents):
if p in outs:
raise StagePathAsOutputError(stage, str(outs[p]))
for stage in stages:
G.add_node(stage)
for dep in stage.deps: for dep in stage.deps:
if dep.path_info is None: if dep.path_info is None:
continue continue
for out_path_info, out in outs.items(): dep_key = dep.path_info.parts
if out_path_info.overlaps(dep.path_info): overlapping = list(n.value for n in outs.prefixes(dep_key))
G.add_node(out.stage) if outs.has_subtrie(dep_key):
G.add_edge(stage, out.stage) overlapping.extend(outs.values(prefix=dep_key))
G.add_edges_from((stage, out.stage) for out in overlapping)
check_acyclic(G) check_acyclic(G)
+1
View File
@@ -77,6 +77,7 @@ install_requires = [
"speedcopy>=2.0.1; sys_platform == 'win32'", "speedcopy>=2.0.1; sys_platform == 'win32'",
"flatten_json>=0.1.6", "flatten_json>=0.1.6",
"texttable>=0.5.2", "texttable>=0.5.2",
"pygtrie==2.3.2",
] ]