jm

Odin for scripts: small packages and a runner, on core: only
Log | Files | Refs | README

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 }