review

review patchsets using your default editor
Log | Files | Refs

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 }