package benchmark_core_diff import "core:diff" import "core:fmt" import "core:log" import "core:math/rand" import "core:strings" import "core:testing" import "core:text/table" import "core:time" // Each diff is timed this many times, and the fastest run is reported. RUNS :: 3 @(rodata) VARIANTS := [?]diff.Options{ {algorithm = .Myers}, {algorithm = .Myers, flags = {.Prune}}, {algorithm = .Minimal}, {algorithm = .Histogram}, {algorithm = .Histogram, flags = {.Prune}}, {algorithm = .Ratcliff_Obershelp}, {algorithm = .Ratcliff_Obershelp, flags = {.Prune}}, {algorithm = .Myers, flags = {.Prune, .Compact}}, // as git --no-indent-heuristic {algorithm = .Histogram, flags = {.Compact}}, // as git --histogram --no-indent-heuristic {algorithm = .Myers, flags = {.Prune, .Compact, .Indent_Heuristic}}, // as plain git diff, for lines only } // Short and common, meaning lines made of them repeat a lot. @(rodata) CODE_WORDS := [?]string{"if", "for", "x", "y", "return", "count", "}", "{", "\t", "// note", "foo(bar)", "Value"} // Enough words for prose to read as text, with plenty repeated. @(rodata) PROSE_WORDS := [?]string{ "the", "a", "of", "and", "to", "in", "is", "that", "it", "was", "for", "on", "are", "with", "as", "they", "be", "at", "one", "have", "this", "from", "by", "hot", "word", "but", "what", "some", "we", "can", "out", "other", "were", "all", "there", "when", "up", "use", "your", "how", "said", "an", "each", "she", "which", "do", "their", "time", "if", "will", "way", "about", "many", "then", "them", "write", "would", "like", "so", } Scenario :: struct { name: string, a, b: string, token: diff.Token, } // Lines that are nearly all different from each other, like most source code. unique_line :: proc(sb: ^strings.Builder) { fmt.sbprintf(sb, "\tvalue_%d := compute(%d, %d)\n", rand.int_max(1_000_000), rand.int_max(1000), rand.int_max(1000)) } // Lines made of a few common words, which repeat many times over. repetitive_line :: proc(sb: ^strings.Builder) { for _ in 0..<1 + rand.int_max(4) { strings.write_string(sb, CODE_WORDS[rand.int_max(len(CODE_WORDS))]) strings.write_byte(sb, ' ') } strings.write_byte(sb, '\n') } // Two texts of `lines` lines, where about one line in `every` is replaced, inserted or removed. edited_lines :: proc(lines, every: int, make_line: proc(sb: ^strings.Builder)) -> (a, b: string) { sa := strings.builder_make(context.temp_allocator) sb := strings.builder_make(context.temp_allocator) for _ in 0.. (a, b: string) { sa := strings.builder_make(context.temp_allocator) prose(&sa, words) a = strings.to_string(sa) sb := strings.builder_make(context.temp_allocator) for word in strings.split_after(a, " ", context.temp_allocator) { if rand.int_max(every) == 0 { strings.write_string(&sb, PROSE_WORDS[rand.int_max(len(PROSE_WORDS))]) strings.write_byte(&sb, ' ') } else { strings.write_string(&sb, word) } } return a, strings.to_string(sb) } lines_scenario :: proc(name: string, lines, every: int, make_line: proc(sb: ^strings.Builder)) -> (sc: Scenario) { sc.name, sc.token = name, .Line sc.a, sc.b = edited_lines(lines, every, make_line) return } // Two texts of unique lines made separately, meaning they have nothing in common. unrelated_scenario :: proc(name: string, lines: int) -> (sc: Scenario) { sc.name, sc.token = name, .Line sc.a, _ = edited_lines(lines, 1, unique_line) sc.b, _ = edited_lines(lines, 1, unique_line) return } prose_scenario :: proc(name: string, words, every: int, token: diff.Token) -> (sc: Scenario) { sc.name, sc.token = name, token sc.a, sc.b = edited_prose(words, every) return } // Times `opts` on `sc`, returning the fastest of `RUNS`, how many tokens changed, and the scratch it needed. time_diff :: proc(sc: Scenario, opts: diff.Options) -> (fastest: time.Duration, changed, scratch: int) { for run in 0.. string { sb := strings.builder_make(context.temp_allocator) for flag in flags { if len(sb.buf) > 0 { strings.write_string(&sb, ", ") } fmt.sbprint(&sb, flag) } return strings.to_string(sb) } report :: proc(title: string, tbl: ^table.Table) { sb := strings.builder_make() defer strings.builder_destroy(&sb) fmt.sbprintln(&sb, title) table.write_plain_table(strings.to_writer(&sb), tbl) log.info(strings.to_string(sb)) } @test benchmark_algorithms :: proc(t: ^testing.T) { rand.reset(t.seed) scenarios := [?]Scenario{ lines_scenario("lines, unique, 0.5% edited", 100_000, 200, unique_line), lines_scenario("lines, repetitive, 0.5% edited", 100_000, 200, repetitive_line), lines_scenario("lines, unique, 10% edited", 20_000, 10, unique_line), unrelated_scenario("lines, nothing in common", 5_000), prose_scenario("words, prose, 2% edited", 50_000, 50, .Word), prose_scenario("runes, prose, 2% edited", 5_000, 50, .Rune), } tbl: table.Table table.init(&tbl) defer table.destroy(&tbl) table.aligned_header_of_values(&tbl, .Right, "Input", "Tokens", "Algorithm", "Flags", "Fastest", "Changed", "Scratch") for sc in scenarios { tokens := diff.token_count(sc.a, sc.token) + diff.token_count(sc.b, sc.token) for opts in VARIANTS { if .Indent_Heuristic in opts.flags && sc.token != .Line { continue // only lines have indentation } fastest, changed, scratch := time_diff(sc, opts) table.aligned_row_of_values(&tbl, .Right, sc.name, tokens, opts.algorithm, flags_name(opts.flags), fmt.tprintf("%v", fastest), changed, fmt.tprintf("%.1f MB", f64(scratch) / 1e6), ) } } report("core:diff algorithms, the fastest of 3 runs each", &tbl) } @test benchmark_generic :: proc(t: ^testing.T) { rand.reset(t.seed) n := 1_000_000 a := make([]int, n) defer delete(a) for &x in a { x = rand.int_max(100_000) } b := make([dynamic]int, 0, n + 1_000) defer delete(b) append(&b, ..a) for _ in 0..<1_000 { switch rand.int_max(3) { case 0: inject_at(&b, rand.int_max(len(b) + 1), rand.int_max(100_000)) case 1: ordered_remove(&b, rand.int_max(len(b))) case 2: b[rand.int_max(len(b))] = rand.int_max(100_000) } } tbl: table.Table table.init(&tbl) defer table.destroy(&tbl) table.aligned_header_of_values(&tbl, .Right, "Algorithm", "Flags", "Fastest", "Changed", "Scratch") for opts in VARIANTS { switch { case opts.algorithm == .Ratcliff_Obershelp: continue // random integers repeat too often for it to be worth timing case .Indent_Heuristic in opts.flags: continue // only text has indentation } fastest: time.Duration changed: int for run in 0..