[ninjago] Drag calculation for critical steps

Test: fx test --host -o ninjagraph_test
Change-Id: Ic586f9140f9104d9dc01f0b272f765ce5624c639
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/442477
Commit-Queue: Jay Zhuang <jayzhuang@google.com>
Reviewed-by: Shai Barack <shayba@google.com>
Testability-Review: Shai Barack <shayba@google.com>
diff --git a/tools/build/ninjago/ninjagraph/ninjagraph.go b/tools/build/ninjago/ninjagraph/ninjagraph.go
index aa10614..ac765ff 100644
--- a/tools/build/ninjago/ninjagraph/ninjagraph.go
+++ b/tools/build/ninjago/ninjagraph/ninjagraph.go
@@ -732,3 +732,93 @@
 	sort.Slice(criticalPath, func(i, j int) bool { return criticalPath[i].Start < criticalPath[j].Start })
 	return criticalPath, nil
 }
+
+// parallelizableEdges returns all edges that can be run in parallel with the
+// input edge.
+func (g *Graph) parallelizableEdges(edge *Edge) ([]*Edge, error) {
+	if edge.Step == nil {
+		return nil, fmt.Errorf("input edge has no step associated to it, this should not happen if edges are correctly populated")
+	}
+
+	latestFinish, err := g.latestFinish(edge)
+	if err != nil {
+		return nil, err
+	}
+
+	var ret []*Edge
+	for _, e := range g.Edges {
+		if e == edge || e.Rule == "phony" || e.Rule == ninjagoArtificialSinkRule {
+			continue
+		}
+		if e.Step == nil {
+			return nil, fmt.Errorf("step is missing on edge, step can be populated by calling `PopulateEdges`")
+		}
+
+		es, err := g.earliestStart(e)
+		if err != nil {
+			return nil, err
+		}
+		lf, err := g.latestFinish(e)
+		if err != nil {
+			return nil, err
+		}
+
+		// TODO(jayzhuang): validate whether the checks below are good enough to get
+		// all parallelizable edges.
+
+		// This check covers 2 possibilities:
+		//
+		// edge:     |oooooooo|
+		// e:    |-----------------|
+		//
+		// or
+		//
+		// edge: |ooooooooo|
+		// e:       |------------|
+		if lf >= latestFinish && es < latestFinish {
+			ret = append(ret, e)
+			continue
+		}
+	}
+	return ret, nil
+}
+
+// drag returns drag of the input edge. Drag is the amount of time an action
+// on the critical path is adding the total build time.
+//
+// NOTE: drag calculation assumes unconstrained parallelism, such that actions
+// can always begin as soon as their inputs are satisfied and never wait to be
+// scheduled on an available machine resources (CPU, RAM, I/O).
+//
+// https://en.wikipedia.org/wiki/Critical_path_drag
+func (g *Graph) drag(edge *Edge) (time.Duration, error) {
+	if edge.Step == nil {
+		return 0, fmt.Errorf("step is missing on edge, step can be populated by calling `PopulateEdges`")
+	}
+
+	tf, err := g.totalFloat(edge)
+	if err != nil {
+		return 0, fmt.Errorf("calculating total float: %w", err)
+	}
+	if tf != 0 {
+		// This edge is not on the critical path, so drag = 0.
+		return 0, fmt.Errorf("drag called for non-critical edge")
+	}
+
+	edges, err := g.parallelizableEdges(edge)
+	if err != nil {
+		return 0, fmt.Errorf("getting parallelizable edges: %w", err)
+	}
+
+	drag := edge.Step.Duration()
+	for _, e := range edges {
+		tf, err := g.totalFloat(e)
+		if err != nil {
+			return 0, fmt.Errorf("calculating total float of parallel edges: %w", err)
+		}
+		if tf < drag {
+			drag = tf
+		}
+	}
+	return drag, nil
+}
diff --git a/tools/build/ninjago/ninjagraph/ninjagraph_test.go b/tools/build/ninjago/ninjagraph/ninjagraph_test.go
index 9dbcc25..8161fde 100644
--- a/tools/build/ninjago/ninjagraph/ninjagraph_test.go
+++ b/tools/build/ninjago/ninjagraph/ninjagraph_test.go
@@ -968,6 +968,25 @@
 	}
 
 	for _, want := range []struct {
+		input           *Edge
+		parallelizables []*Edge
+	}{
+		{input: edge1},
+		{input: edge2, parallelizables: []*Edge{edge6, edge8}},
+		{input: edge3, parallelizables: []*Edge{edge6, edge8}},
+		{input: edge4, parallelizables: []*Edge{edge7, edge8}},
+		{input: edge5},
+	} {
+		got, err := g.parallelizableEdges(want.input)
+		if err != nil {
+			t.Errorf("parallelizableEdges(edge outputting %v) error: %v", want.input.Outputs, err)
+		}
+		if !cmp.Equal(got, want.parallelizables, cmpopts.IgnoreUnexported(Edge{})) {
+			t.Errorf("parallelizableEdges(edge outputting %v) = %s, want: %s", want.input.Outputs, edgesToStr(got), edgesToStr(want.parallelizables))
+		}
+	}
+
+	for _, want := range []struct {
 		input          *Edge
 		earliestStart  time.Duration
 		earliestFinish time.Duration
@@ -1056,6 +1075,29 @@
 			}
 		}
 	}
+
+	for _, want := range []struct {
+		input *Edge
+		err   bool
+		drag  time.Duration
+	}{
+		{input: edge1, drag: 10},
+		{input: edge2, drag: 15},
+		{input: edge3, drag: 5},
+		{input: edge4, drag: 5},
+		{input: edge5, drag: 20},
+		{input: edge6, err: true},
+		{input: edge7, err: true},
+		{input: edge8, err: true},
+	} {
+		got, err := g.drag(want.input)
+		if (err != nil) != want.err {
+			t.Errorf("drag(edge outputting %v) got error: %v, want error: %t", want.input.Outputs, err, want.err)
+		}
+		if got != want.drag {
+			t.Errorf("drag(edge outputting %v) = %s, want: %s", want.input.Outputs, got, want.drag)
+		}
+	}
 }
 
 func edgesToStr(edges []*Edge) string {