bench
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Geoff Hammond

//@version=5

// @description A simple banchmark library to analyse script performance and bottlenecks
library("bench")


// █▄▄ █▀▀ █▄ █ █▀▀ █ █   █   █ █▄▄ █▀█ ▄▀█ █▀█ █▄█
// █▄█ ██▄ █ ▀█ █▄▄ █▀█   █▄▄ █ █▄█ █▀▄ █▀█ █▀▄  █

// @TODO: If intervals are used, process the full span?


// ██╗  ██╗███████╗██╗     ██████╗ ███████╗██████╗  ██████╗
// ██║  ██║██╔════╝██║     ██╔══██╗██╔════╝██╔══██╗██╔════╝
// ███████║█████╗  ██║     ██████╔╝█████╗  ██████╔╝╚█████╗
// ██╔══██║██╔══╝  ██║     ██╔═══╝ ██╔══╝  ██╔══██╗ ╚═══██╗
// ██║  ██║███████╗███████╗██║     ███████╗██║  ██║██████╔╝
// ╚═╝  ╚═╝╚══════╝╚══════╝╚═╝     ╚══════╝╚═╝  ╚═╝╚═════╝

// QUICK REFERENCE

// // Looping benchmark style
// benchmark = bench.new(samples = 500, loops = 5000)
// data = array.new_int()
//
// if bench.start(benchmark)
//     while bench.loop(benchmark)
//         array.unshift(data, timenow)
//
//     bench.mark(benchmark)
//     while bench.loop(benchmark)
//         array.unshift(data, timenow)
//
//     bench.mark(benchmark)
//     while bench.loop(benchmark)
//         array.unshift(data, timenow)
//
//     bench.stop(benchmark)
//     bench.reference(array.get(data, 0))
// bench.report(benchmark, '1x array.unshift()')
//
// // Linear benchmark style
// benchmark = bench.new()
// data = array.new_int()
//
// bench.start(benchmark)
// for i = 0 to 1000
//     array.unshift(data, timenow)
//
// bench.mark(benchmark)
// for i = 0 to 1000
//     array.unshift(data, timenow)
//
// bench.stop(benchmark)
// bench.reference(array.get(data, 0))
// bench.report(benchmark,'1000x array.unshift()')


// ██╗ ███╗   ███╗ ██████╗   █████╗  ██████╗  ████████╗  ██████╗
// ██║ ████╗ ████║ ██╔══██╗ ██╔══██╗ ██╔══██╗ ╚══██╔══╝ ██╔════╝
// ██║ ██╔████╔██║ ██████╔╝ ██║  ██║ ██████╔╝    ██║    ╚█████╗
// ██║ ██║╚██╔╝██║ ██╔═══╝  ██║  ██║ ██╔══██╗    ██║     ╚═══██╗
// ██║ ██║ ╚═╝ ██║ ██║      ╚█████╔╝ ██║  ██║    ██║    ██████╔╝
// ╚═╝ ╚═╝     ╚═╝ ╚═╝       ╚════╝  ╚═╝  ╚═╝    ╚═╝    ╚═════╝

import GeoffHammond/assert/4


//  ██████╗  ██╗       █████╗  ██████╗   █████╗  ██╗       ██████╗
// ██╔════╝  ██║      ██╔══██╗ ██╔══██╗ ██╔══██╗ ██║      ██╔════╝
// ██║  ██╗  ██║      ██║  ██║ ██████╦╝ ███████║ ██║      ╚█████╗
// ██║  ╚██╗ ██║      ██║  ██║ ██╔══██╗ ██╔══██║ ██║       ╚═══██╗
// ╚██████╔╝ ███████╗ ╚█████╔╝ ██████╦╝ ██║  ██║ ███████╗ ██████╔╝
//  ╚═════╝  ╚══════╝  ╚════╝  ╚═════╝  ╚═╝  ╚═╝ ╚══════╝ ╚═════╝

var COUNTER = 0
var REQUIRED = 1
var STATE = 2
var LOOPS = 3
var MARKS = 4
var DATA = 5

var NEW = 0
var STARTED = 1
var MARKED = 2
var STOPPED = 3
var FINISHED = 4

var RATE = 0
var MEAN = 1
var VARIANCE = 2
var STDEV = 3
var MIN = 4
var MAX = 5
var FIRST = 6
var LAST = 7
var DELTA = 8
var SUM = 9

var RESULT_FIELDS = 10

var FORMAT_REPORT_HEADER = '{0}Benchmark Results:'
var FORMAT_VARIANCE = '{0,number,#.#}%'
var FORMAT_RATE = '~{0,number,#.##}{1}'
var FORMAT_MICROTIME = '{0,number,#.###}{1}'


// ███████╗ ██╗   ██╗ ███╗  ██╗  █████╗  ████████╗ ██╗  █████╗  ███╗  ██╗  ██████╗
// ██╔════╝ ██║   ██║ ████╗ ██║ ██╔══██╗ ╚══██╔══╝ ██║ ██╔══██╗ ████╗ ██║ ██╔════╝
// █████╗   ██║   ██║ ██╔██╗██║ ██║  ╚═╝    ██║    ██║ ██║  ██║ ██╔██╗██║ ╚█████╗
// ██╔══╝   ██║   ██║ ██║╚████║ ██║  ██╗    ██║    ██║ ██║  ██║ ██║╚████║  ╚═══██╗
// ██║      ╚██████╔╝ ██║ ╚███║ ╚█████╔╝    ██║    ██║ ╚█████╔╝ ██║ ╚███║ ██████╔╝
// ╚═╝       ╚═════╝  ╚═╝  ╚══╝  ╚════╝     ╚═╝    ╚═╝  ╚════╝  ╚═╝  ╚══╝ ╚═════╝

array_increment(arr, offset) => array.set(arr, offset, array.get(arr, offset) + 1), arr

array_decrement(arr, offset) => array.set(arr, offset, array.get(arr, offset) - 1), arr

array_last(arr) => array.get(arr, array.size(arr) - 1)

// @TODO: remove this when next version of asserts is published
only(int repeat = 1) =>
    var done = 0
    if done < repeat
        done += 1
        true
    else
        false

pad(string value, int amount = 1) =>
    spaces = array.new_string(amount, ' ')
    str.format('{0}{1}{0}', array.join(spaces, ''), value)


// ███╗   ███╗  █████╗  ██████╗  ██╗   ██╗ ██╗      ███████╗
// ████╗ ████║ ██╔══██╗ ██╔══██╗ ██║   ██║ ██║      ██╔════╝
// ██╔████╔██║ ██║  ██║ ██║  ██║ ██║   ██║ ██║      █████╗
// ██║╚██╔╝██║ ██║  ██║ ██║  ██║ ██║   ██║ ██║      ██╔══╝
// ██║ ╚═╝ ██║ ╚█████╔╝ ██████╔╝ ╚██████╔╝ ███████╗ ███████╗
// ╚═╝     ╚═╝  ╚════╝  ╚═════╝   ╚═════╝  ╚══════╝ ╚══════╝

add_compiler_reference(string value) => label.delete(label.new(bar_index, 0, value))

process_results(int[] benchmark) =>
    required = array.get(benchmark, REQUIRED)
    state = array.get(benchmark, STATE)
    loops = array.get(benchmark, LOOPS)
    marks = array.get(benchmark, MARKS)

    if state < STOPPED
        runtime.error('[Bench:process_results] State is not stopped, please check order of execution.')

    sample_size = marks + 2 // the number of measurements in each sample
    data = array.slice(benchmark, DATA, array.size(benchmark))
    samples = array.size(data) / sample_size

    if required != samples
        runtime.error(str.format('[Bench:process_results] Required sample count "{0}" does not equal captured sample count "{1}"', required, samples))

    results = array.new_float()
    for i = 0 to marks
        interval = array.new_int()
        int first = na
        int last = na
        float delta = na

        for s = 0 to samples - 1
            start = array.get(data, s * sample_size + i)
            end = array.get(data, s * sample_size + i + 1)
            array.push(interval, end - start)
            if s == 0
                first := end - start
            if s == samples - 1
                last := end - start

        size = array.size(interval)
        _delta = array.new_float()
        if size > 1
            for j = 0 to size - 2
                array.push(_delta, array.get(interval, j + 1) - array.get(interval, j))
            delta := array.avg(_delta) / loops
        else
            delta := 0

        mean = array.avg(interval) / loops
        min = array.min(interval) / loops
        max = array.max(interval) / loops
        array.push(results, 1000 / mean)                                        // RATE
        array.push(results, mean)                                               // MEAN
        array.push(results, (math.max(max - mean, mean - min) / mean) * 100)    // VARIANCE
        array.push(results, array.stdev(interval) / loops)                      // STDEV
        array.push(results, min)                                                // MIN
        array.push(results, max)                                                // MAX
        array.push(results, first / loops)                                      // FIRST
        array.push(results, last / loops)                                       // LAST
        array.push(results, delta)                                              // DELTA
        array.push(results, array.sum(interval))                                // SUM
    results

format_rate(float value) =>
    if value >= 995000000
        str.format(FORMAT_RATE, value / 1000000000, 'B')
    else if value >= 995000
        str.format(FORMAT_RATE, value / 1000000, 'M')
    else if value >= 995
        str.format(FORMAT_RATE, value / 1000, 'K')
    else
        str.format(FORMAT_RATE, value, '')

format_microtime(float value) =>
    if math.abs(value) < 0.000001
        str.format(FORMAT_MICROTIME, value * 1000000000, 'ps')
    else if math.abs(value) < 0.001
        str.format(FORMAT_MICROTIME, value * 1000000, 'ns')
    else if math.abs(value) < 1
        str.format(FORMAT_MICROTIME, value * 1000, 'μs')
    else if math.abs(value) >= 1000
        str.format(FORMAT_MICROTIME, value / 1000, 's')
    else
        str.format(FORMAT_MICROTIME, value, 'ms')


// ██╗      ██╗ ██████╗  ██████╗   █████╗  ██████╗  ██╗   ██╗
// ██║      ██║ ██╔══██╗ ██╔══██╗ ██╔══██╗ ██╔══██╗ ╚██╗ ██╔╝
// ██║      ██║ ██████╦╝ ██████╔╝ ███████║ ██████╔╝  ╚████╔╝
// ██║      ██║ ██╔══██╗ ██╔══██╗ ██╔══██║ ██╔══██╗   ╚██╔╝
// ███████╗ ██║ ██████╦╝ ██║  ██║ ██║  ██║ ██║  ██║    ██║
// ╚══════╝ ╚═╝ ╚═════╝  ╚═╝  ╚═╝ ╚═╝  ╚═╝ ╚═╝  ╚═╝    ╚═╝

// @function Initialises a new benchmark array
// @param samples int, the number of bars in which to collect samples
// @param loops int, the number of loops to execute within each sample
// @returns int[], the benchmark array
export new(int samples = na, int loops = 1) =>
    if samples <= 1 and loops <= 1
        runtime.error('[Bench:new] Trying to run a benchmark with a sample size of 1 is just silly!')
    varip benchmark = array.new_int(2, samples)
    counter = array.get(benchmark, COUNTER)
    if na(counter) or counter == samples
        array.push(benchmark, NEW)   // initial state
        array.push(benchmark, loops) // number of loops in each sample
        array.push(benchmark, 0)     // number of marks
    if na(samples)
        if barstate.islastconfirmedhistory
            array.set(benchmark, COUNTER, -1)
            array.set(benchmark, REQUIRED, bar_index)
            array.set(benchmark, STATE, FINISHED)
        else
            array.set(benchmark, COUNTER, 1)
    benchmark

// @function Determing if the benchmarks state is active
// @param benchmark int[], the benchmark array
// @returns bool, true only if the state is active
export active(int[] benchmark) =>
    state = array.get(benchmark, STATE)
    state > NEW and state < STOPPED

// @function Start recording a benchmark from this point
// @param benchmark int[], the benchmark array
// @returns bool, true only if the benchmark is unfinished
export start(int[] benchmark) =>
    counter = array.get(benchmark, COUNTER)

    if counter > 0 or na(array.get(benchmark, REQUIRED))
        if active(benchmark)
            runtime.error('[Bench:start] Timer has been previously started but not stopped! Please add a bench.stop() call.')
        array.set(benchmark, STATE, STARTED)
        array.set(benchmark, MARKS, 0)
        array_decrement(benchmark, COUNTER)
        array.push(benchmark, timenow)
        true
    else if counter == 0
        array.set(benchmark, STATE, FINISHED)
        array_decrement(benchmark, COUNTER)
        false
    else
        false

// @function Returns true until call count exceeds bench.new(loop) variable
// @param benchmark int[], the benchmark array
// @returns bool, true while looping
export loop(int[] benchmark) =>
    varip count = 0
    if active(benchmark) and count < array.get(benchmark, LOOPS)
        count += 1
        true
    else
        count := 0
        false

// @function Add a compiler reference to the chart so the calculations don't get optimised away
// @param number float, a non-zero numeric value to reference
// @param string string, a non-empty string value to reference
export reference(float number = 0, string string = '') =>
    if number != 0
        add_compiler_reference(str.tostring(number))
    if string != ''
        add_compiler_reference(string)

// @function Marks the end of one recorded interval and the start of the next
// @param benchmark int[], the benchmark array
// @param number float, a numeric value to reference
// @param string string, a string value to reference
export mark(int[] benchmark, float number = na, string string = na) =>
    if active(benchmark) and array.get(benchmark, COUNTER) >= 0
        array.push(benchmark, timenow)
        array.set(benchmark, STATE, MARKED)
        array_increment(benchmark, MARKS)
        reference(number, string)

// @function Stop the benchmark, ending the final interval
// @param benchmark int[], the benchmark array
// @param number float, a numeric value to reference
// @param string string, a string value to reference
export stop(int[] benchmark, float number = na, string string = na) =>
    if active(benchmark) and array.get(benchmark, COUNTER) >= 0
        array.set(benchmark, STATE, STOPPED)
        reference(number, string)
        array.push(benchmark, timenow)

// @param Prints the benchmarks results to the screen
// @param benchmark int[], the benchmark array
// @param title string, add a custom title to the report
// @param text_size string, the text size of the log console (global size vars)
// @param position string, the position of the log console (global position vars)
export report(int[] benchmark, string title = na, string text_size = na, string position = na) =>
    if barstate.islastconfirmedhistory
        counter = array.get(benchmark, COUNTER)
        required = array.get(benchmark, REQUIRED)
        loops = array.get(benchmark, LOOPS)
        marks = array.get(benchmark, MARKS)

        if counter > 0
            runtime.error('[Bench:report] Counter is not zero, there may not be enough bars on the chart, or bench.new(samples) is too high.')

        results = process_results(benchmark)
        size = array.size(results) / RESULT_FIELDS
        var tbl = table.new(na(position) ? position.middle_left : position, size + 1, 14, #000000, #888888, 1)
        if size
            _text_size = na(text_size) ? size.normal : text_size
            header_color = #FFFFFF
            heading_color = #FFFFFF
            cell_color = #adf5f7

            table.cell(tbl, 0, 0, str.format(FORMAT_REPORT_HEADER, na(title) ? '' : title + '\n\n'), text_color=header_color, text_halign=text.align_left, text_size=size.large, bgcolor=na)
            table.cell(tbl, 0, 1, 'Total Executions:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 2, 'Timed Intervals:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 3, 'Interval:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=#111111)
            table.cell(tbl, 0, 4, 'Rate (per sec):', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 5, 'Mean:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 6, 'Variance:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 7, 'Std Dev:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 8, 'Fastest:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 9, 'Slowest:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 10, 'First:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 11, 'Last:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 12, 'Delta:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 0, 13, 'Total Time:', text_color=heading_color, text_halign=text.align_right, text_size=_text_size, bgcolor=na)

            table.cell(tbl, 1, 1, str.format('{0}', required * loops), text_color=cell_color, text_halign=text.align_left, text_size=_text_size, bgcolor=na)
            table.cell(tbl, 1, 2, str.format('{0}', marks + 1), text_color=cell_color, text_halign=text.align_left, text_size=_text_size, bgcolor=na)

            for i = 0 to size - 1
                cell_bgcolor = i % 2 == 0 ? #060606 : na
                rate = array.get(results, i * RESULT_FIELDS + RATE)
                mean = array.get(results, i * RESULT_FIELDS + MEAN)
                variance = array.get(results, i * RESULT_FIELDS + VARIANCE)
                stdev = array.get(results, i * RESULT_FIELDS + STDEV)
                min = array.get(results, i * RESULT_FIELDS + MIN)
                max = array.get(results, i * RESULT_FIELDS + MAX)
                first = array.get(results, i * RESULT_FIELDS + FIRST)
                last = array.get(results, i * RESULT_FIELDS + LAST)
                delta = array.get(results, i * RESULT_FIELDS + DELTA)
                sum = array.get(results, i * RESULT_FIELDS + SUM)

                table.cell(tbl, i + 1, 3, pad(str.format('#{0}', i + 1)), text_color=heading_color, text_halign=text.align_center, text_size=_text_size, bgcolor=#111111)
                table.cell(tbl, i + 1, 4, pad(format_rate(rate)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 5, pad(format_microtime(mean)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 6, pad(str.format(FORMAT_VARIANCE, variance)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 7, pad(format_microtime(stdev)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 8, pad(format_microtime(min)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 9, pad(format_microtime(max)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 10, pad(format_microtime(first)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 11, pad(format_microtime(last)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 12, pad(format_microtime(delta)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)
                table.cell(tbl, i + 1, 13, pad(format_microtime(sum)), text_color=cell_color, text_halign=text.align_center, text_size=_text_size, bgcolor=cell_bgcolor)


// ██╗   ██╗ ███╗  ██╗ ██╗ ████████╗    ████████╗ ███████╗  ██████╗ ████████╗  ██████╗
// ██║   ██║ ████╗ ██║ ██║ ╚══██╔══╝    ╚══██╔══╝ ██╔════╝ ██╔════╝ ╚══██╔══╝ ██╔════╝
// ██║   ██║ ██╔██╗██║ ██║    ██║          ██║    █████╗   ╚█████╗     ██║    ╚█████╗
// ██║   ██║ ██║╚████║ ██║    ██║          ██║    ██╔══╝    ╚═══██╗    ██║     ╚═══██╗
// ╚██████╔╝ ██║ ╚███║ ██║    ██║          ██║    ███████╗ ██████╔╝    ██║    ██████╔╝
//  ╚═════╝  ╚═╝  ╚══╝ ╚═╝    ╚═╝          ╚═╝    ╚══════╝ ╚═════╝     ╚═╝    ╚═════╝

// @function Cache module unit tests, for inclusion in parent script test suite. Usage: bench.unittest_bench(__ASSERTS)
// @param case string[], the current test case and array of previous unit tests (__ASSERTS)
export unittest_bench(string[] case) =>
    assert.new_case(case, 'Bench:array_increment')
    assert.array_equal(array_increment(array.from(1, 2), 0), array.from(2, 2), case)
    assert.array_equal(array_increment(array.from(1, 2), 1), array.from(1, 3), case)

    assert.new_case(case, 'Bench:array_decrement')
    assert.array_equal(array_decrement(array.from(1, 2), 0), array.from(0, 2), case)
    assert.array_equal(array_decrement(array.from(1, 2), 1), array.from(1, 1), case)

    assert.new_case(case, 'Bench:array_last')
    assert.equal(array_last(array.from(1, 2, 3)), 3, case)

    assert.new_case(case, 'Bench:new')
    assert.equal(array.size(new()), 5, case)
    assert.equal(array.get(new(), COUNTER), 1, case)
    assert.nan(array.get(new(), REQUIRED), case)
    assert.equal(array.get(new(1000), COUNTER), 1000, case)
    assert.equal(array.get(new(1000), REQUIRED), 1000, case)
    assert.equal(array.get(new(5000), REQUIRED), 5000, case)
    assert.equal(array.get(new(1000), STATE), NEW, case)
    assert.equal(array.get(new(1000), LOOPS), 1, case)
    assert.equal(array.get(new(5000, 100), LOOPS), 100, case)
    assert.equal(array.get(new(1000), MARKS), 0, case)
    assert.equal(array.get(new(5000, 100), MARKS), 0, case)
    // @TODO: Bench:new barstate.islastconfirmedhistory section untested

    assert.new_case(case, 'Bench:start')
    benchmark = new()
    timestamp = timenow
    array.set(benchmark, MARKS, 1)
    assert.is_true(start(benchmark), case)
    assert.equal(array.size(benchmark), 6, case)
    assert.equal(array.get(benchmark, COUNTER), 0, case)
    assert.nan(array.get(benchmark, REQUIRED) , case)
    assert.equal(array.get(benchmark, STATE), STARTED, case)
    assert.equal(array.get(benchmark, MARKS), 0, case)
    assert.greater_or_equal(array_last(benchmark), timestamp, case)
    benchmark := new(1000)
    assert.is_true(start(benchmark), case)
    assert.equal(array.get(benchmark, COUNTER), 999, case)
    assert.equal(array.get(benchmark, REQUIRED), 1000, case)
    assert.equal(array.get(benchmark, STATE), STARTED, case)
    assert.equal(array.get(benchmark, MARKS), 0, case)
    array.set(benchmark, COUNTER, 0)
    array.set(benchmark, STATE, STOPPED)
    assert.is_false(start(benchmark), case)
    assert.equal(array.get(benchmark, COUNTER), -1, case)
    assert.equal(array.get(benchmark, STATE), FINISHED, case)
    assert.is_false(start(benchmark), case)
    assert.equal(array.get(benchmark, COUNTER), -1, case)

    assert.new_case(case, 'Bench:loop')
    i = 0
    while loop(benchmark)
        i += 1
    assert.equal(i, 0, case)
    array.set(benchmark, COUNTER, 10)
    array.set(benchmark, STATE, STARTED)
    i := 0
    while loop(benchmark)
        i += 1
    assert.equal(i, 1, case)
    array.set(benchmark, LOOPS, 10)
    i := 0
    while loop(benchmark)
        i += 1
    assert.equal(i, 10, case)

    assert.new_case(case, 'Bench:reference')
    // Not sure this function is really testable :/

    assert.new_case(case, 'Bench:mark')
    timestamp := timenow
    mark(benchmark)
    assert.equal(array.size(benchmark), 7, case)
    assert.equal(array.get(benchmark, STATE), MARKED, case)
    assert.equal(array.get(benchmark, MARKS), 1, case)
    assert.greater_or_equal(array_last(benchmark), timestamp, case)

    assert.new_case(case, 'Bench:stop')
    stop(benchmark)
    timestamp := timenow
    assert.equal(array.size(benchmark), 8, case)
    assert.equal(array.get(benchmark, STATE), STOPPED, case)
    assert.greater_or_equal(array_last(benchmark), timestamp, case)

    assert.new_case(case, 'Bench:process_results')
    benchmark := new(3, 10)
    array.set(benchmark, COUNTER, -1)
    array.set(benchmark, STATE, FINISHED)
    array.set(benchmark, MARKS, 1)
    array.push(benchmark, 100), array.push(benchmark, 120), array.push(benchmark, 150)
    array.push(benchmark, 201), array.push(benchmark, 220), array.push(benchmark, 252)
    array.push(benchmark, 302), array.push(benchmark, 320), array.push(benchmark, 357)
    results = process_results(benchmark)
    assert.equal(array.size(results), RESULT_FIELDS * 2, case)
    sum = 20.0 + 19.0 + 18.0
    mean = sum / 3.0 / 10.0
    assert.equal(array.get(results, RATE), 1000.0 / mean, case)
    assert.equal(array.get(results, MEAN), mean, case)
    assert.equal(array.get(results, VARIANCE), 0.1 / mean * 100.0, case)
    assert.equal(array.get(results, STDEV), array.stdev(array.from(20, 19, 18)) / 10, case)
    assert.equal(array.get(results, MIN), 1.8, case)
    assert.equal(array.get(results, MAX), 2, case)
    assert.equal(array.get(results, FIRST), 2, case)
    assert.equal(array.get(results, LAST), 1.8, case)
    assert.equal(array.get(results, DELTA), -0.1, case)
    assert.equal(array.get(results, SUM), sum, case)
    sum := 30.0 + 32.0 + 37.0
    mean := sum / 3.0 / 10.0
    assert.equal(array.get(results, RESULT_FIELDS + RATE), 1000.0 / mean, case)
    assert.equal(array.get(results, RESULT_FIELDS + MEAN), mean, case)
    assert.equal(array.get(results, RESULT_FIELDS + VARIANCE), 0.4 / mean * 100.0, case)
    assert.equal(array.get(results, RESULT_FIELDS + STDEV), array.stdev(array.from(30, 32, 37)) / 10, case)
    assert.equal(array.get(results, RESULT_FIELDS + MIN), 3, case)
    assert.equal(array.get(results, RESULT_FIELDS + MAX), 3.7, case)
    assert.equal(array.get(results, RESULT_FIELDS + FIRST), 3, case)
    assert.equal(array.get(results, RESULT_FIELDS + LAST), 3.7, case)
    assert.equal(array.get(results, RESULT_FIELDS + DELTA), 0.35, case)
    assert.equal(array.get(results, RESULT_FIELDS + SUM), sum, case)

    assert.new_case(case, 'Bench:format_rate')
    assert.str_equal(format_rate(1000000000), '~1B', case)
    assert.str_equal(format_rate(995000000), '~0.99B', case)
    assert.str_equal(format_rate(994999999), '~995M', case)
    assert.str_equal(format_rate(994990000), '~994.99M', case)
    assert.str_equal(format_rate(1000000), '~1M', case)
    assert.str_equal(format_rate(995000), '~0.99M', case)
    assert.str_equal(format_rate(994999), '~995K', case)
    assert.str_equal(format_rate(994990), '~994.99K', case)
    assert.str_equal(format_rate(1000), '~1K', case)
    assert.str_equal(format_rate(995), '~0.99K', case)
    assert.str_equal(format_rate(994.99), '~994.99', case)

    assert.new_case(case, 'Bench:format_microtime')
    assert.str_equal(format_microtime(0.000000001), '1ps', case)
    assert.str_equal(format_microtime(0.000000999), '999ps', case)
    assert.str_equal(format_microtime(0.000001), '1ns', case)
    assert.str_equal(format_microtime(-0.000001), '-1ns', case)
    assert.str_equal(format_microtime(0.000001001), '1.001ns', case)
    assert.str_equal(format_microtime(0.000999), '999ns', case)
    assert.str_equal(format_microtime(0.001), '1μs', case)
    assert.str_equal(format_microtime(-0.001), '-1μs', case)
    assert.str_equal(format_microtime(0.001001), '1.001μs', case)
    assert.str_equal(format_microtime(0.999), '999μs', case)
    assert.str_equal(format_microtime(1), '1ms', case)
    assert.str_equal(format_microtime(-1), '-1ms', case)
    assert.str_equal(format_microtime(1.001), '1.001ms', case)
    assert.str_equal(format_microtime(999), '999ms', case)
    assert.str_equal(format_microtime(1000), '1s', case)
    assert.str_equal(format_microtime(-1000), '-1s', case)
    assert.str_equal(format_microtime(1001), '1.001s', case)

    assert.new_case(case, 'Bench:execution')
    benchmark := new(2, 10)
    i := 0
    while only(3) // simulating three bars
        if start(benchmark)
            while loop(benchmark)
                i += 1
            mark(benchmark)
            while loop(benchmark)
                i += 1
            stop(benchmark)
    assert.equal(i, 40, case)
    assert.equal(array.size(benchmark), 11, case)
    assert.equal(array.get(benchmark, STATE), FINISHED, case)
    assert.equal(array.get(benchmark, MARKS), 1, case)

// @function Run the bench module unit tests as a stand alone. Usage: bench.unittest()
// @param verbose bool, optionally disable the full report to only display failures
export unittest(bool verbose = true) =>
    if assert.once()
        __ASSERTS = assert.init()
        unittest_bench(__ASSERTS)
        assert.report(__ASSERTS, verbose, position=position.top_right)


// ███████╗██╗░░██╗░█████╗░███╗░░░███╗██████╗░██╗░░░░░███████╗░██████╗
// ██╔════╝╚██╗██╔╝██╔══██╗████╗░████║██╔══██╗██║░░░░░██╔════╝██╔════╝
// █████╗░░░╚███╔╝░███████║██╔████╔██║██████╔╝██║░░░░░█████╗░░╚█████╗░
// ██╔══╝░░░██╔██╗░██╔══██║██║╚██╔╝██║██╔═══╝░██║░░░░░██╔══╝░░░╚═══██╗
// ███████╗██╔╝╚██╗██║░░██║██║░╚═╝░██║██║░░░░░███████╗███████╗██████╔╝
// ╚══════╝╚═╝░░╚═╝╚═╝░░╚═╝╚═╝░░░░░╚═╝╚═╝░░░░░╚══════╝╚══════╝╚═════╝░

examples() =>
    group1 = 'Example 1: Artificial looping benchmark (multiple samples per bar)'
    example1 = input.bool(true, 'Run?', group=group1)
    samples = input.int(500, 'Number of samples (bars) to run?', group=group1, minval=1, step=10)
    loops = input.int(5000, 'Number of loops to run in each sample?', group=group1, minval=1, step=10)

    group2 = 'Example 2: Integrated linear benchmark (one sample per bar)'
    example2 = input.bool(false, 'Run? (disable 1 first)', group=group2)
    unshifts = input.int(3000, 'Number of unshifts to bench on each bar?', group=group2, minval=1, step=10)

    if example1
        // When doing artifical benchmarks, the faster the operation under test, the more loops are required to give an accurate result.
        // This in turn likely means lowering the sample size to only run a limited number of times.
        benchmark = new(samples = samples, loops = loops)
        data = array.new_int()

        // Here we limit execution to the chosen number of samples
        if start(benchmark)
            // We use the loop function to execute the code under tests for the required number of loops
            while loop(benchmark)
                // Code under test for interval 1 (unshift into 0 length array)
                array.unshift(data, timenow)

            mark(benchmark)

            // Loops can be used multiple times and must be run for all marked intervals
            while loop(benchmark)
                // Code under test for interval 2 (unshift into 5000 length array)
                array.unshift(data, timenow)

            mark(benchmark)

            while loop(benchmark)
                // Code under test for interval 3 (unshift into 10000 length array)
                array.unshift(data, timenow)

            stop(benchmark)

            // We must add a reference to the array, so the compiler doesn't ignore it (most artifical benchmarks will need this)
            reference(array.get(data, 0))

        report(benchmark, '1x array.unshift()')

    else if example2
        // This style of benchmarking will only be useful if you have a highly computationally expensive script.
        // As the default benchmark will only run a single sample on every bar until barstate.islastconfirmedhistory.
        benchmark = new()
        data = array.new_int()

        start(benchmark)

        // Expensive code under test for interval 1 (3000 unshifts onto a 0 length array)
        for i = 0 to unshifts
            array.unshift(data, timenow)

        // mark(benchmark)
        //
        // Example further code under test for interval 2 (1000 unshifts onto a 3000 length array)
        // for i = 0 to 1000
        //     array.unshift(data, timenow)

        stop(benchmark)

        // We must add a reference to the array, so the compiler doesn't ignore it (your code may not need this if it already draws)
        reference(array.get(data, 0))

        report(benchmark, str.format('{0}x array.unshift()', unshifts))


// ██████╗  ██╗   ██╗ ███╗  ██╗
// ██╔══██╗ ██║   ██║ ████╗ ██║
// ██████╔╝ ██║   ██║ ██╔██╗██║
// ██╔══██╗ ██║   ██║ ██║╚████║
// ██║  ██║ ╚██████╔╝ ██║ ╚███║
// ╚═╝  ╚═╝  ╚═════╝  ╚═╝  ╚══╝

unittest()

examples()

//         _____
//      _.'_____`._
//    .'.-'  12 `-.`.
//   /,' 11      1 `.\
//  // 10      /   2 \\
// ;;         /       ::
// || 9  ----O      3 ||
// ::                 ;;
//  \\ 8           4 //
//   \`. 7       5 ,'/
//    '.`-.__6__.-'.'
//     ((-._____.-))
//     _))       ((_    Is benching just an illusion too?
//    '--'       '--'
