bench.odin (4332B)
1 /* 2 Package bench measures the deterministic checks over as much history as 3 there is: run them over the last N commits and count what fires. A rule 4 that fires on a tenth of ordinary commits is not measuring what it claims 5 to. 6 */ 7 package bench 8 9 import "core:fmt" 10 import "core:slice" 11 import "core:strings" 12 13 import "../change" 14 import "../check" 15 import "../git" 16 import "../tree" 17 18 // Options are what a bench is narrowed to: how many commits, whose, and 19 // which rule to list the commits of. 20 Options :: struct { 21 n: int, 22 author: string, 23 rule: string, 24 } 25 26 // Fire is one commit a rule fired on. 27 Fire :: struct { 28 commit, subject, message: string, 29 } 30 31 // run measures the checks over the repository's recent commits and 32 // reports the fire rate per rule, or the commits one rule fired on. 33 // progress is told how far along the measuring is. 34 run :: proc( 35 root: string, 36 opts: Options, 37 progress: proc(done, total: int), 38 allocator := context.allocator, 39 ) -> ( 40 out: string, 41 err: string, 42 ) { 43 context.allocator = allocator 44 listing := make([dynamic]string, context.temp_allocator) 45 append(&listing, "rev-list", "--no-merges", fmt.tprintf("-%d", opts.n)) 46 if opts.author != "" { 47 append(&listing, strings.concatenate({"--author=", opts.author}, context.temp_allocator)) 48 } 49 append(&listing, "HEAD") 50 commits, listed := git.lines(root, listing[:], context.temp_allocator) 51 if !listed || len(commits) == 0 { 52 return "", "no commits to measure" 53 } 54 fires := make(map[string][dynamic]Fire, context.temp_allocator) 55 measured := 0 56 for commit, i in commits { 57 if progress != nil { 58 progress(i + 1, len(commits)) 59 } 60 rev := fmt.tprintf("%s^..%s", commit, commit) 61 c, gathered := change.gather(rev, root, context.temp_allocator) 62 if !gathered { 63 continue // A root commit has no parent to diff against. 64 } 65 t, at_ok := tree.at(root, rev, context.temp_allocator) 66 if !at_ok { 67 continue 68 } 69 change.read(&c, t, context.temp_allocator) 70 c.index, _ = change.index(t, context.temp_allocator) 71 findings := check.run( 72 check.scope_of(&c, t, context.temp_allocator), 73 context.temp_allocator, 74 ) 75 tree.close(t) // One tree per commit; a bench must not keep them all. 76 measured += 1 77 seen := make(map[string]bool, context.temp_allocator) 78 subject := strings.trim_space(c.message) 79 if nl := strings.index_byte(subject, '\n'); nl >= 0 { 80 subject = subject[:nl] 81 } 82 for f in findings { 83 if seen[f.rule] { 84 continue 85 } 86 seen[f.rule] = true 87 list := fires[f.rule] 88 if list.allocator.procedure == nil { 89 list = make([dynamic]Fire, context.temp_allocator) 90 } 91 append(&list, Fire{commit[:min(8, len(commit))], subject, f.message}) 92 fires[strings.clone(f.rule, context.temp_allocator)] = list 93 } 94 } 95 b := strings.builder_make(allocator) 96 if opts.rule != "" { 97 listed_fires := fires[opts.rule] 98 for f in listed_fires { 99 fmt.sbprintf( 100 &b, 101 "%s %s\n %s\n", 102 f.commit, 103 f.subject, 104 f.message[:min(160, len(f.message))], 105 ) 106 } 107 fmt.sbprintf( 108 &b, 109 "\n%s fired on %d of %d commits\n", 110 opts.rule, 111 len(listed_fires), 112 measured, 113 ) 114 return strings.to_string(b), "" 115 } 116 rules, _ := slice.map_keys(fires, context.temp_allocator) 117 slice.sort_by_cmp(rules, proc(a, b: string) -> slice.Ordering { 118 return .Less if a < b else (.Greater if a > b else .Equal) 119 }) 120 // The most frequent first, the rest by name. 121 counted = &fires 122 slice.stable_sort_by(rules, proc(a, b: string) -> bool { 123 return len(fires_of(a)) > len(fires_of(b)) 124 }) 125 counted = nil 126 fmt.sbprintf(&b, "%d commits measured\n\n", measured) 127 fmt.sbprintf(&b, "%-28s %6s %s\n", "rule", "fires", "rate") 128 for r in rules { 129 count := strings.right_justify( 130 fmt.tprintf("%d", len(fires[r])), 131 6, 132 " ", 133 context.temp_allocator, 134 ) 135 rate := strings.right_justify( 136 fmt.tprintf("%.1f%%", 100 * f64(len(fires[r])) / f64(measured)), 137 5, 138 " ", 139 context.temp_allocator, 140 ) 141 fmt.sbprintf(&b, "%-28s %s %s\n", r, count, rate) 142 } 143 if len(rules) == 0 { 144 strings.write_string(&b, "nothing fired\n") 145 } 146 return strings.to_string(b), "" 147 } 148 149 // The fires under measurement, reachable from the sort's comparator, 150 // which cannot capture them. 151 @(private) 152 counted: ^map[string][dynamic]Fire 153 154 @(private) 155 fires_of :: proc(rule: string) -> [dynamic]Fire { 156 if counted == nil { 157 return {} 158 } 159 return counted[rule] 160 }