1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
package eval
import "testing"
// The funnel exists so that NrAttrsets/NrAttrsInAttrsets/NrListElems can be
// compared with C++ arithmetically, and its value depends entirely on two rules
// that are easy to get wrong in opposite directions: capacity is counted rather
// than length, and the empty set is not counted at all. Both are C++'s, not
// choices, and neither is visible in any other assertion — a violation would
// leave every drvPath identical and only make the counters quietly wrong, which
// is the failure mode this whole section of OPTIMIZATION.md is about.
// TestNewBindingsCountsCapacityNotLength pins the rule that makes the counter
// comparable to Lix's: allocBindings adds `capacity` to nrAttrsInAttrsets
// (attr-set.cc:22) before a single attr is inserted, so an over-allocated set
// counts its slack.
//
// This matters concretely for `//`: mergeBindings reserves len(b1)+len(b2) and
// leaves duplicates unused, exactly as C++ buildBindings does, and
// OPTIMIZATION.md compares that reserved figure against Lix's
// nrOpUpdateValuesCopied. Counting length instead would silently change what
// that comparison means.
func TestNewBindingsCountsCapacityNotLength(t *testing.T) {
s := &EvalState{}
b := newBindings(s, 10)
if got := len(b.attrs); got != 0 {
t.Errorf("len = %d, want 0 — newBindings reserves, it does not fill", got)
}
if got := cap(b.attrs); got != 10 {
t.Errorf("cap = %d, want 10", got)
}
// One attr inserted, ten counted.
b.attrs = append(b.attrs, Attr{Name: 1})
if s.Stats.NrAttrsets != 1 {
t.Errorf("NrAttrsets = %d, want 1", s.Stats.NrAttrsets)
}
if s.Stats.NrAttrsInAttrsets != 10 {
t.Errorf("NrAttrsInAttrsets = %d, want 10 (capacity, not the 1 filled)",
s.Stats.NrAttrsInAttrsets)
}
}
// TestNewBindingsZeroIsEmptyAndUncounted pins the other half: C++ allocBindings
// returns &Bindings::EMPTY for capacity 0 and returns *before* the two
// increments (attr-set.cc:17), so a set with no attributes is not an attrset as
// far as the statistics are concerned.
//
// Counting it would be the more "obvious" behaviour and would put gonix
// permanently above Lix by however many empty sets a workload builds, which on
// nixpkgs is not a small number.
func TestNewBindingsZeroIsEmptyAndUncounted(t *testing.T) {
s := &EvalState{}
b := newBindings(s, 0)
if b != EmptyBindings {
t.Errorf("newBindings(s, 0) = %p, want the EmptyBindings singleton %p",
b, EmptyBindings)
}
if s.Stats.NrAttrsets != 0 || s.Stats.NrAttrsInAttrsets != 0 {
t.Errorf("empty set counted: NrAttrsets=%d NrAttrsInAttrsets=%d, want 0 and 0",
s.Stats.NrAttrsets, s.Stats.NrAttrsInAttrsets)
}
}
// TestNewBindingsLenFillsAndCounts checks that the by-index variant differs from
// newBindings in length only, and counts the same. It exists because the two are
// easy to swap at a call site and the mistake is silent: a length-0 Bindings
// where the caller assigns by index panics immediately, but a length-N one where
// the caller appends produces a set with N zero attrs in front of the real
// ones — well-formed, wrong, and sorted into plausibility by the Sort that
// follows.
func TestNewBindingsLenFillsAndCounts(t *testing.T) {
s := &EvalState{}
b := newBindingsLen(s, 3)
if got := len(b.attrs); got != 3 {
t.Errorf("len = %d, want 3", got)
}
if s.Stats.NrAttrsets != 1 || s.Stats.NrAttrsInAttrsets != 3 {
t.Errorf("NrAttrsets=%d NrAttrsInAttrsets=%d, want 1 and 3",
s.Stats.NrAttrsets, s.Stats.NrAttrsInAttrsets)
}
// Zero must still be the shared empty singleton, so that the two
// constructors agree on the one case where returning a fresh object would
// be a silent extra allocation as well as an extra count.
s2 := &EvalState{}
if newBindingsLen(s2, 0) != EmptyBindings {
t.Error("newBindingsLen(s, 0) did not return the EmptyBindings singleton")
}
if s2.Stats.NrAttrsets != 0 {
t.Errorf("NrAttrsets = %d, want 0", s2.Stats.NrAttrsets)
}
}
// TestNewBindingsOfCountsItsAttrs covers the fixed-literal sites — `{ success,
// value }`, `{ right, wrong }`, `{ file, line, column }`. They were already
// exact before the funnel, having been written as []Attr literals, which made
// them the shape most likely to be left outside it on the grounds that they had
// nothing to fix.
func TestNewBindingsOfCountsItsAttrs(t *testing.T) {
s := &EvalState{}
b := newBindingsOf(s,
Attr{Name: 2, Value: VNullSingleton},
Attr{Name: 1, Value: VNullSingleton},
)
if got := len(b.attrs); got != 2 {
t.Fatalf("len = %d, want 2", got)
}
if s.Stats.NrAttrsets != 1 || s.Stats.NrAttrsInAttrsets != 2 {
t.Errorf("NrAttrsets=%d NrAttrsInAttrsets=%d, want 1 and 2",
s.Stats.NrAttrsets, s.Stats.NrAttrsInAttrsets)
}
// Source order is preserved; sorting is the caller's job, as it was when
// these were literals. If the constructor sorted, a caller that omitted
// its own Sort would appear to work.
if b.attrs[0].Name != 2 {
t.Errorf("Attrs[0].Name = %d, want 2 — newBindingsOf must not sort",
b.attrs[0].Name)
}
}
// TestNewListCountsElements pins nrListElems against C++ newList, which counts
// `nrListElems += size` (eval.cc:846) — elements, not lists. The empty list is
// counted, unlike the empty attrset, because C++ newList has no zero-capacity
// early return; that asymmetry between the two is Lix's and is the sort of
// detail that gets "tidied" into consistency by someone reading only this side.
func TestNewListCountsElements(t *testing.T) {
s := &EvalState{}
newList(s, []Value{VNullSingleton, VNullSingleton, VNullSingleton})
newList(s, []Value{VNullSingleton})
if s.Stats.NrListElems != 4 {
t.Errorf("NrListElems = %d, want 4 — elements across all lists, not lists",
s.Stats.NrListElems)
}
// An empty list contributes zero either way, so this pins that newList does
// not somehow count the list itself.
s2 := &EvalState{}
newList(s2, nil)
if s2.Stats.NrListElems != 0 {
t.Errorf("NrListElems = %d after an empty list, want 0", s2.Stats.NrListElems)
}
}
|