flow_test.odin (5660B)
1 package flow 2 3 import "core:testing" 4 5 // Counting alone cannot tell "every item once" from "one item twice and another 6 // never", so each worker also sums the items it saw. The two together pin the run 7 // down: the count proves how many were handled and the sum proves which. 8 @(private = "file") 9 Tally :: struct { 10 handled: int, 11 sum: int, 12 } 13 14 @(private = "file") 15 ITEMS :: 100_000 16 17 @(private = "file") 18 count_up :: proc(item: int, tally: ^Tally) -> bool { 19 tally.handled += 1 20 tally.sum += item 21 return true 22 } 23 24 @(private = "file") 25 sequence :: proc() -> []int { 26 items := make([]int, ITEMS, context.temp_allocator) 27 for i in 0 ..< ITEMS { 28 items[i] = i 29 } 30 return items 31 } 32 33 @(private = "file") 34 busy :: proc(tallies: []Tally) -> (n: int) { 35 for tally in tallies { 36 if tally.handled > 0 { 37 n += 1 38 } 39 } 40 return 41 } 42 43 @(private = "file") 44 totals :: proc(tallies: []Tally) -> (handled, sum: int) { 45 for t in tallies { 46 handled += t.handled 47 sum += t.sum 48 } 49 return 50 } 51 52 @(test) 53 test_each_handles_every_item_exactly_once :: proc(t: ^testing.T) { 54 tallies := make([]Tally, 8, context.temp_allocator) 55 each(sequence(), tallies, count_up) 56 57 handled, sum := totals(tallies) 58 testing.expect_value(t, handled, ITEMS) 59 testing.expect_value(t, sum, ITEMS * (ITEMS - 1) / 2) 60 } 61 62 @(test) 63 test_each_shares_the_work_out :: proc(t: ^testing.T) { 64 // Each item has to cost appreciably more than starting a thread, or the calling 65 // thread finishes the whole run before the others are scheduled and the split 66 // says nothing. That is a property of the work, not of the claiming. 67 tallies := make([]Tally, 4, context.temp_allocator) 68 items := make([]int, 32, context.temp_allocator) 69 each( 70 items, 71 tallies, 72 proc(item: int, tally: ^Tally) -> bool { 73 acc := 0 74 for i in 0 ..< 1_000_000 { 75 acc += i ~ item 76 } 77 tally.handled += 1 78 tally.sum += acc & 1 // consume acc so the loop cannot be optimised away 79 return true 80 }, 81 ) 82 83 testing.expect(t, busy(tallies) > 1, "work stayed on a single worker") 84 } 85 86 @(test) 87 test_each_with_one_slot_runs_inline :: proc(t: ^testing.T) { 88 tallies := make([]Tally, 1, context.temp_allocator) 89 each(sequence(), tallies, count_up) 90 91 handled, sum := totals(tallies) 92 testing.expect_value(t, handled, ITEMS) 93 testing.expect_value(t, sum, ITEMS * (ITEMS - 1) / 2) 94 } 95 96 @(test) 97 test_each_stops_when_work_returns_false :: proc(t: ^testing.T) { 98 // One slot keeps this deterministic: with several workers a few more items 99 // finish after the decision to stop, which is the documented behaviour. 100 tallies := make([]Tally, 1, context.temp_allocator) 101 each(sequence(), tallies, proc(item: int, tally: ^Tally) -> bool { 102 if item == 10 { 103 return false 104 } 105 tally.handled += 1 106 return true 107 }) 108 testing.expect_value(t, tallies[0].handled, 10) 109 } 110 111 @(test) 112 test_each_stops_early_across_workers :: proc(t: ^testing.T) { 113 tallies := make([]Tally, 8, context.temp_allocator) 114 each(sequence(), tallies, proc(item: int, tally: ^Tally) -> bool { 115 if item > 100 { 116 return false 117 } 118 tally.handled += 1 119 return true 120 }) 121 122 handled, _ := totals(tallies) 123 testing.expect(t, handled > 0, "no item was handled before the stop") 124 testing.expect(t, handled < ITEMS, "stopping did not cut the run short") 125 } 126 127 @(test) 128 test_each_tolerates_empty_input :: proc(t: ^testing.T) { 129 tallies := make([]Tally, 4, context.temp_allocator) 130 each([]int{}, tallies, count_up) 131 handled, _ := totals(tallies) 132 testing.expect_value(t, handled, 0) 133 134 // No slots means no worker can own state, so there is nothing to run on. 135 each(sequence(), []Tally{}, count_up) 136 } 137 138 @(test) 139 test_width_never_exceeds_the_work :: proc(t: ^testing.T) { 140 // However wide the machine, three items can only keep three workers busy. 141 testing.expect_value(t, width(3, .Io), 3) 142 testing.expect_value(t, width(1, .Io), 1) 143 // No work still has to give a runnable answer rather than zero. 144 testing.expect_value(t, width(0), 1) 145 } 146 147 @(test) 148 test_width_respects_the_limit :: proc(t: ^testing.T) { 149 testing.expect_value(t, width(1000, .Io, limit = 4), 4) 150 // The limit is a ceiling, not a target: fewer items still win. 151 testing.expect_value(t, width(2, .Io, limit = 4), 2) 152 } 153 154 @(test) 155 test_width_grows_with_waiting :: proc(t: ^testing.T) { 156 // Plenty of work, so the load is the only thing deciding the answer. 157 cpu := width(10_000, .Cpu) 158 mixed := width(10_000, .Mixed) 159 io := width(10_000, .Io) 160 testing.expect(t, cpu >= 1) 161 testing.expect(t, mixed > cpu, "mixed work should outnumber cpu bound work") 162 testing.expect(t, io > mixed, "waiting work should outnumber mixed work") 163 // Derived from the core count, so a huge input cannot produce a huge width. 164 testing.expect(t, io < 10_000, "width ran away with the input") 165 } 166 167 @(test) 168 test_each_ignores_a_pathological_width :: proc(t: ^testing.T) { 169 // Asking for thousands of workers is a mistake, not an instruction. The run has 170 // to stay correct and the pool has to stay within what the machine can use. 171 tallies := make([]Tally, 4000, context.temp_allocator) 172 each(sequence(), tallies, count_up) 173 174 handled, sum := totals(tallies) 175 testing.expect_value(t, handled, ITEMS) 176 testing.expect_value(t, sum, ITEMS * (ITEMS - 1) / 2) 177 testing.expect( 178 t, 179 busy(tallies) <= width(ITEMS, .Mixed), 180 "the pool grew past the default ceiling", 181 ) 182 } 183 184 @(test) 185 test_each_caps_by_the_load_it_is_given :: proc(t: ^testing.T) { 186 // Cpu is the narrowest tier, so it has to hold the pool below the default. 187 tallies := make([]Tally, 4000, context.temp_allocator) 188 each(sequence(), tallies, count_up, .Cpu) 189 190 handled, _ := totals(tallies) 191 testing.expect_value(t, handled, ITEMS) 192 testing.expect(t, busy(tallies) <= width(ITEMS, .Cpu), "the load did not reach the ceiling") 193 }