refactor(server): Expose layer grouping logic via a function

Refactors the layer grouping package (which previously compiled to a
separate binary) to expose the layer grouping logic via a function
instead.

This is the next step towards creating layers inside of the server
component instead of in Nix.

Relates to #50.
This commit is contained in:
Vincent Ambo 2019-09-29 23:57:56 +01:00 committed by Vincent Ambo
parent 8c79d085ae
commit 9c3c622403

View file

@ -1,6 +1,6 @@
// This program reads an export reference graph (i.e. a graph representing the // This package reads an export reference graph (i.e. a graph representing the
// runtime dependencies of a set of derivations) created by Nix and groups them // runtime dependencies of a set of derivations) created by Nix and groups it in
// in a way that is likely to match the grouping for other derivation sets with // a way that is likely to match the grouping for other derivation sets with
// overlapping dependencies. // overlapping dependencies.
// //
// This is used to determine which derivations to include in which layers of a // This is used to determine which derivations to include in which layers of a
@ -9,8 +9,8 @@
// # Inputs // # Inputs
// //
// * a graph of Nix runtime dependencies, generated via exportReferenceGraph // * a graph of Nix runtime dependencies, generated via exportReferenceGraph
// * a file containing absolute popularity values of packages in the // * popularity values of each package in the Nix package set (in the form of a
// Nix package set (in the form of a direct reference count) // direct reference count)
// * a maximum number of layers to allocate for the image (the "layer budget") // * a maximum number of layers to allocate for the image (the "layer budget")
// //
// # Algorithm // # Algorithm
@ -103,9 +103,6 @@
package layers package layers
import ( import (
"encoding/json"
"flag"
"io/ioutil"
"log" "log"
"regexp" "regexp"
"sort" "sort"
@ -114,9 +111,11 @@ import (
"gonum.org/v1/gonum/graph/simple" "gonum.org/v1/gonum/graph/simple"
) )
// closureGraph represents the structured attributes Nix outputs when asking it // RuntimeGraph represents structured information from Nix about the runtime
// for the exportReferencesGraph of a list of derivations. // dependencies of a derivation.
type exportReferences struct { //
// This is generated in Nix by using the exportReferencesGraph feature.
type RuntimeGraph struct {
References struct { References struct {
Graph []string `json:"graph"` Graph []string `json:"graph"`
} `json:"exportReferencesGraph"` } `json:"exportReferencesGraph"`
@ -135,14 +134,14 @@ type exportReferences struct {
// of the nixpkgs tree. // of the nixpkgs tree.
type Popularity = map[string]int type Popularity = map[string]int
// layer represents the data returned for each layer that Nix should // Layer represents the data returned for each layer that Nix should
// build for the container image. // build for the container image.
type layer struct { type Layer struct {
Contents []string `json:"contents"` Contents []string `json:"contents"`
mergeRating uint64 mergeRating uint64
} }
func (a layer) merge(b layer) layer { func (a Layer) merge(b Layer) Layer {
a.Contents = append(a.Contents, b.Contents...) a.Contents = append(a.Contents, b.Contents...)
a.mergeRating += b.mergeRating a.mergeRating += b.mergeRating
return a return a
@ -209,7 +208,7 @@ func insertEdges(graph *simple.DirectedGraph, cmap *map[string]*closure, node *c
} }
// Create a graph structure from the references supplied by Nix. // Create a graph structure from the references supplied by Nix.
func buildGraph(refs *exportReferences, pop *Popularity) *simple.DirectedGraph { func buildGraph(refs *RuntimeGraph, pop *Popularity) *simple.DirectedGraph {
cmap := make(map[string]*closure) cmap := make(map[string]*closure)
graph := simple.NewDirectedGraph() graph := simple.NewDirectedGraph()
@ -259,7 +258,7 @@ func buildGraph(refs *exportReferences, pop *Popularity) *simple.DirectedGraph {
// Extracts a subgraph starting at the specified root from the // Extracts a subgraph starting at the specified root from the
// dominator tree. The subgraph is converted into a flat list of // dominator tree. The subgraph is converted into a flat list of
// layers, each containing the store paths and merge rating. // layers, each containing the store paths and merge rating.
func groupLayer(dt *flow.DominatorTree, root *closure) layer { func groupLayer(dt *flow.DominatorTree, root *closure) Layer {
size := root.Size size := root.Size
contents := []string{root.Path} contents := []string{root.Path}
children := dt.DominatedBy(root.ID()) children := dt.DominatedBy(root.ID())
@ -273,7 +272,7 @@ func groupLayer(dt *flow.DominatorTree, root *closure) layer {
children = append(children, dt.DominatedBy(child.ID())...) children = append(children, dt.DominatedBy(child.ID())...)
} }
return layer{ return Layer{
Contents: contents, Contents: contents,
// TODO(tazjin): The point of this is to factor in // TODO(tazjin): The point of this is to factor in
// both the size and the popularity when making merge // both the size and the popularity when making merge
@ -288,10 +287,10 @@ func groupLayer(dt *flow.DominatorTree, root *closure) layer {
// //
// Layers are merged together until they fit into the layer budget, // Layers are merged together until they fit into the layer budget,
// based on their merge rating. // based on their merge rating.
func dominate(budget int, graph *simple.DirectedGraph) []layer { func dominate(budget int, graph *simple.DirectedGraph) []Layer {
dt := flow.Dominators(graph.Node(0), graph) dt := flow.Dominators(graph.Node(0), graph)
var layers []layer var layers []Layer
for _, n := range dt.DominatedBy(dt.Root().ID()) { for _, n := range dt.DominatedBy(dt.Root().ID()) {
layers = append(layers, groupLayer(&dt, n.(*closure))) layers = append(layers, groupLayer(&dt, n.(*closure)))
} }
@ -313,40 +312,10 @@ func dominate(budget int, graph *simple.DirectedGraph) []layer {
return layers return layers
} }
func main() { // GroupLayers applies the algorithm described above the its input and returns a
graphFile := flag.String("graph", ".attrs.json", "Input file containing graph") // list of layers, each consisting of a list of Nix store paths that it should
popFile := flag.String("pop", "popularity.json", "Package popularity data") // contain.
outFile := flag.String("out", "layers.json", "File to write layers to") func GroupLayers(refs *RuntimeGraph, pop *Popularity, budget int) []Layer {
layerBudget := flag.Int("budget", 94, "Total layer budget available") graph := buildGraph(refs, pop)
flag.Parse() return dominate(budget, graph)
// Parse graph data
file, err := ioutil.ReadFile(*graphFile)
if err != nil {
log.Fatalf("Failed to load input: %s\n", err)
}
var refs exportReferences
err = json.Unmarshal(file, &refs)
if err != nil {
log.Fatalf("Failed to deserialise input: %s\n", err)
}
// Parse popularity data
popBytes, err := ioutil.ReadFile(*popFile)
if err != nil {
log.Fatalf("Failed to load input: %s\n", err)
}
var pop Popularity
err = json.Unmarshal(popBytes, &pop)
if err != nil {
log.Fatalf("Failed to deserialise input: %s\n", err)
}
graph := buildGraph(&refs, &pop)
layers := dominate(*layerBudget, graph)
j, _ := json.Marshal(layers)
ioutil.WriteFile(*outFile, j, 0644)
} }