blob: e9e00fb0242da5947813f334c5f25af855a4e7ac [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package submit
import (
"fmt"
"sort"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/cv/internal/changelist"
"go.chromium.org/luci/cv/internal/common"
"go.chromium.org/luci/cv/internal/run"
)
// ComputeOrder computes the best order for submission of CLs based on
// their dependencies. Dependency cycle is broken by arbitrarily but
// deterministically breaking soft requirement dependencies and approximately
// minimizing their number.
//
// Returns error if duplicate CLs are provided or the hard requirement
// dependencies solely form a cycle (This should not be possible within current
// CV context as hard requirement deps can only be established if CLs follow
// Git's child -> parent relationship).
//
// Overall, this is as hard as a minimum feedback arc set, which is NP-Hard:
// https://en.wikipedia.org/wiki/Feedback_arc_set#Minimum_feedback_arc_set
// However, approximation is fine within CV context so long as hard
// dependencies are satisfied.
func ComputeOrder(cls []*run.RunCL) ([]*run.RunCL, error) {
ret, _, err := compute(cls)
return ret, err
}
type brokenDep struct {
src, dest common.CLID
}
// compute is DFS-based toposort which backtracks if a cycle is found and
// removes the last-followed soft requirement dependency.
//
// Denoting V=len(cls), E=number of dependency edges (bounded by O(V^2)),
// the runtime is bounded by O(V*E), which worst case can be O(V**3).
//
// This can be reduced to O(max(E, V)) by first computing isomorphic
// graph of connected components via hard requirement deps and then
// running toposorts on a graph of components AND inside each component
// separately.
//
// Alternative idea for improvement is to could accumulate a skiplist style
// data structure during iteration too, i.e. from node X what is the next
// guaranteed-reachable node Y. Then during backtracking, just skip forward
// over chains of hard requirement changes. Also, with a reciprocal structure,
// during 'skip back' phase one can skip directly to the nearest soft dep.
func compute(cls []*run.RunCL) ([]*run.RunCL, int, error) {
ret := make([]*run.RunCL, 0, len(cls))
remainingCLs := make(map[common.CLID]*run.RunCL, len(cls))
for _, cl := range cls {
if _, ok := remainingCLs[cl.ID]; ok {
return nil, 0, errors.Reason("duplicate cl: %d", cl.ID).Err()
}
remainingCLs[cl.ID] = cl
}
brokenDeps := map[brokenDep]struct{}{}
cycleDetector := common.CLIDsSet{}
// visit returns whether a cycle was detected that can't be resolved yet.
var visit func(cl *run.RunCL) bool
visit = func(cl *run.RunCL) bool {
if _, ok := remainingCLs[cl.ID]; !ok {
return false
}
if cycleDetector.Has(cl.ID) {
return true
}
cycleDetector.Add(cl.ID)
for _, dep := range cl.Detail.GetDeps() {
// TODO(yiwzhang): Strictly speaking, the time complexity for hashtable
// access is not strictly O(1) considering the time spent on hash func
// and comparing the output value (Denoted it as HASH(key)). Therefore,
// the time we spent on the following lines is O(E*V*HASH) which is
// O(V^3*HASH) in worst case. This can be improved by pre-computing
// Deps of each CLs to replace its key with the index of the CL
// (requires O(E*HASH)). Then, we can make remainingCLs a bitmask so
// that the time spent on the following line will reduce to O(E*V*1).
// We are gonna defer this improvement till the struct for `Dep` is
// clear to us.
depCLID := common.CLID(dep.GetClid())
if _, ok := remainingCLs[depCLID]; !ok {
continue
}
if _, ok := brokenDeps[brokenDep{cl.ID, depCLID}]; ok {
continue
}
for visit(remainingCLs[depCLID]) {
// Going deeper forms a cycle which can't be broken up at deeper levels.
switch dep.GetKind() {
case changelist.DepKind_SOFT:
case changelist.DepKind_HARD:
// Can't break it at this level either, backtrack.
cycleDetector.Del(cl.ID)
return true
default:
panic(fmt.Errorf("unknown dep kind %s", dep.GetKind()))
}
// Greedily break cycle by removing this dep. This is why this
// algorithm is only an approximation.
brokenDeps[brokenDep{cl.ID, depCLID}] = struct{}{}
break
}
}
cycleDetector.Del(cl.ID)
delete(remainingCLs, cl.ID)
ret = append(ret, cl)
return false
}
sortedCLs := make([]*run.RunCL, len(cls))
copy(sortedCLs, cls)
sort.SliceStable(sortedCLs, func(i, j int) bool {
return sortedCLs[i].ID < sortedCLs[j].ID
})
for _, cl := range sortedCLs {
if visit(cl) {
// A cycle was formed via hard requirement deps, which should not
// happen.
return nil, 0, errors.Reason("cycle detected for cl: %d", cl.ID).Err()
}
}
return ret, len(brokenDeps), nil
}