Profiling: array of UDTs vs UDT of arrays
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © moebius1977


//@version=5
// @description Here I am trying to test which is faster, an array of user-defined type (UDT) objects vs an object with many child arrays. 
// The data to process - total 9 values, 4 floats, 4 strings and 1 int.
// The UDT for array of UDT objects stores 3 float fields and 3 string fields as child fields and  1 float, 1 int and 1 string fields within a child UDT.
// The competing UDT is storing all the same data in individual arrays (total 9 arrays). 
// The test task is of three stages: 
// 1. Populate array(s) with some (timenow) values  - in the options you can choose how many values to push into the array/arrays. Note that max size of array(s) is set independently, so you can push 1000 of elements into an array capped at 100 max size and as new elements will be pushed (added to the end) the old exceeding elements will be shifted (removed from the beginning) 
// 2. Write - write to random elements of the array.
// 3. Read - read from random elements of the array.
// In the options you can how many times per bar to run each of the steps (same number for each step).
// 
// I tested by adding three indicators to the chart and choosing different options for each: 
// 1. Array of UDT's where writing is done by creating a new UDT from the values and then using set(udt)
// 2. Array of UDT's where writing is dome by setting the value of each of the properties of the UDT individually (saving time on creating of a new object). 
// 3. UDT of arrays. 
// As of 16 Arpil 2023 UDT of arrays seems about 200-30% faster than array of UDT's with setting each property without creating new UDT object.

indicator("Profiling: array of UDTs vs UDT of arrays")

import PineCoders/LibraryStopwatch/1 as sw
// loopCount = input.int(1, "Loop iterations per bar", minval = 0, step = 10000)
var loopCount = 1

[timePerBarInMs, totalTimeInMs, barsTimed, barsNotTimed] = sw.stopwatchStats()
msElapsed = sw.stopwatch()

//============================================================
// ————— Script code to time.
//============================================================
// ————— Script code to time.

//----------- ================= -----------
//----------- TYPE udt  -----------{

type udtChild
    float   f1
    int     i1
    string  s1

type udt
    float       f1
    float       f2
    float       f3
    string      s1
    string      s2
    string      s3
    udtChild    udtChild1


method push(udt[] this, udt val,  int maxSize) =>
    this.push(val)
    while this.size() > maxSize 
        this.shift()


// ^^^^^^ END OF  TYPE udt  ^^^^^^ }
//----------- ================= -----------


// udtObj  = udt.new(udtChild1 = udtChild.new())
udt [] arrayOfUDTsObj = array.new<udt>()

//----------- ================= -----------
//----------- TYPE udtOfArrays  -----------{

type udtOfArrays   
    float   [] arF1
    float   [] arF2
    float   [] arF3
    string  [] arS1
    string  [] arS2
    string  [] arS3
    float   [] arUdtChild_f1
    int     [] arUdtChild_i1
    string  [] arUdtChild_s1

method init(udtOfArrays this) =>
    this.arF1 := array.new<float>()
    this.arF2 := array.new<float>()
    this.arF3 := array.new<float>()
    this.arS1 := array.new<string>()
    this.arS2 := array.new<string>()
    this.arS3 := array.new<string>()
    this.arUdtChild_f1 := array.new<float>()
    this.arUdtChild_i1 := array.new<int>()
    this.arUdtChild_s1 := array.new<string>()
    this


method shift(udtOfArrays this) =>
    this.arF1.shift()
    this.arF2.shift()
    this.arF3.shift()
    this.arS1.shift()
    this.arS2.shift()
    this.arS3.shift()
    this.arUdtChild_f1.shift()
    this.arUdtChild_i1.shift()
    this.arUdtChild_s1.shift()

method size(udtOfArrays this) => this.arF1.size()

method push(udtOfArrays this
          , float f1 
          , float f2 
          , float f3 
          , string s1 
          , string s2 
          , string s3  
          , float ucf1
          , int uci1
          , string ucs1
          , int maxSize
          ) =>

    this.arF1.push(f1)
    this.arF2.push(f2)
    this.arF3.push(f3)
    this.arS1.push(s1)
    this.arS2.push(s2)
    this.arS3.push(s3)
    this.arUdtChild_f1.push(ucf1)
    this.arUdtChild_i1.push(uci1)
    this.arUdtChild_s1.push(ucs1)

    while this.size() > maxSize
        this.shift()


method clear(udtOfArrays this) =>
    this.arF1.clear()
    this.arF2.clear()
    this.arF3.clear()
    this.arS1.clear()
    this.arS2.clear()
    this.arS3.clear()    
    this.arUdtChild_f1.clear()    
    this.arUdtChild_i1.clear()    
    this.arUdtChild_s1.clear()    


method get(udtOfArrays this, int id) => 
    [this.arF1.get(id)
      , this.arF2.get(id)
      , this.arF3.get(id)
      , this.arS1.get(id)
      , this.arS2.get(id)
      , this.arS3.get(id)    
      , this.arUdtChild_f1.get(id)   
      , this.arUdtChild_i1.get(id)   
      , this.arUdtChild_s1.get(id)]


method set(udtOfArrays this, int id
          , float f1 
          , float f2 
          , float f3 
          , string s1 
          , string s2 
          , string s3  
          , float ucf1
          , int uci1
          , string ucs1
          ) =>
    this.arF1.set(id, f1)
    this.arF2.set(id, f2)
    this.arF3.set(id, f3)
    this.arS1.set(id, s1)
    this.arS2.set(id, s2)
    this.arS3.set(id, s3)    
    this.arUdtChild_f1.set(id, ucf1)   
    this.arUdtChild_i1.set(id, uci1)   
    this.arUdtChild_s1.set(id, ucs1)


// ^^^^^^ END OF  TYPE udtOfArrays  ^^^^^^ }
//----------- ================= -----------

udtOfArrays udtOfArraysObj = udtOfArrays.new().init() 

var OPTION_1 = "Array of UDTs"
var OPTION_2 = "UDT of arrays"

var SET_OPTION_1 = "set( udt.new(...))"
var SET_OPTION_2 = "assigning to each field without creating new UDT object"


N = input.int(200, "Inner loop iterations, N")
i_maxSize = input.int(100, "Arrays' max size")
i_option = input.string(OPTION_1, title = "Option to test", options= [OPTION_1, OPTION_2])
i_set_option = input.string(SET_OPTION_1, title = "Option to test", options= [SET_OPTION_1, SET_OPTION_2])

sOpt = i_option == OPTION_2 ? OPTION_2 
                              : (OPTION_1 + ": " + (i_set_option == SET_OPTION_1 ? SET_OPTION_1 : SET_OPTION_2))



total = 0. // something to output to make compiler think that that every single get() is important and not to ignore the code




for j = 1 to loopCount // loop one of the options below

    // --- OPTION 1 --- {
    if i_option == OPTION_1

        // populate
        for i = 0 to N-1
            arrayOfUDTsObj.push(
                  udt.new(
                      timenow
                      , timenow
                      , timenow
                      , str.tostring(timenow)
                      , str.tostring(timenow)
                      , str.tostring(timenow)
                      , udtChild.new( 
                          math.random(timenow)
                          , int(math.random(timenow))
                          , str.tostring(timenow)
                          )
                      )
                  , i_maxSize
                  )
        
        // write
        for i = 0 to N-1 
            if i_set_option == SET_OPTION_1
                arrayOfUDTsObj.set(math.floor(math.random(0, i_maxSize)),
                      udt.new(
                              timenow
                              , timenow
                              , timenow
                              , str.tostring(timenow)
                              , str.tostring(timenow)
                              , str.tostring(timenow)
                              , udtChild.new( 
                                  math.random(timenow)
                                  , int(math.random(timenow))
                                  , str.tostring(timenow)
                              )
                          )
                      )
            else if i_set_option == SET_OPTION_2
                obj = arrayOfUDTsObj.get(math.floor(math.random(0, i_maxSize)))
                obj.f1                  := timenow
                obj.f2                  := timenow
                obj.f3                  := timenow
                obj.s1                  := str.tostring(timenow)
                obj.s2                  := str.tostring(timenow)
                obj.s3                  := str.tostring(timenow)
                obj.udtChild1.f1        := math.random(timenow)
                obj.udtChild1.i1        := int(math.random(timenow))
                obj.udtChild1.s1        := str.tostring(timenow)


        // read
        for i = 0 to N-1 
            obj = arrayOfUDTsObj.get(math.floor(math.random(0, i_maxSize)))
            f1 = obj.f1
            f2 = obj.f2
            f3 = obj.f3
            s1 = obj.s1
            s2 = obj.s2
            s3 = obj.s3
            ucf1 = obj.udtChild1.f1                                                      
            uci1 = obj.udtChild1.i1                                                      
            ucs1 = obj.udtChild1.s1
            total += f1+f2+f3+ucf1+uci1


    // END OF OPTION 1}


    //  --- OPTION 2 --- {
    else if i_option == OPTION_2

        // managing 2 simple type arrays is about 500 times faster than creating a new UDT object and pushing it to UDT array

        // populate
        for i = 0 to N-1
            udtOfArraysObj.push(
                  timenow
                  , timenow
                  , timenow
                  , str.tostring(timenow)
                  , str.tostring(timenow)
                  , str.tostring(timenow)
                  , math.random(timenow)
                  , int(math.random(timenow))
                  , str.tostring(timenow)
                  , i_maxSize
                  )

        // write
        for i = 0 to N-1 
            udtOfArraysObj.set(
                  math.floor(math.random(0, i_maxSize))
                  , timenow
                  , timenow
                  , timenow
                  , str.tostring(timenow)
                  , str.tostring(timenow)
                  , str.tostring(timenow)
                  , math.random(timenow)
                  , int(math.random(timenow))
                  , str.tostring(timenow)
                  )


        // read
        for i = 0 to N-1 
            [f1, f2, f3, s1, s2, s3, ucf1, uci1, ucs1] = udtOfArraysObj.get(math.floor(math.random(0, i_maxSize)))
            total += f1+f2+f3+ucf1+uci1

    // END OF OPTION 2}

// something to output to make compiler think that that every single get() is important and not to ignore the code
plotchar(total, "total", "", location = location.top)



// —————————— Display results (code from Stopwatch library except for sOpt)
// ————— Print table at the end of chart.
if barstate.islast
    var table tbl = table.new(position.middle_right, 1, 1)
    var txt = sOpt + "\n" +
              str.tostring(timePerBarInMs, "ms/bar: #.######\n") +
              str.tostring(totalTimeInMs, "Total time (ms): #,###.######\n") + 
              str.tostring(barsTimed + barsNotTimed, "Bars analyzed: #")
    table.cell(tbl, 0, 0, txt, bgcolor = color.yellow, text_halign = text.align_right)

// ————— Plot elapsed time.
plot(totalTimeInMs,  "totalTimeInMs", color.gray)
plot(msElapsed,      "msElapsed",     color.new(color.gray, 70), 6)



// ————— Print Data Window values.
plotchar(timePerBarInMs,    "Avg time / bar (ms)",  "", location.top)
plotchar(totalTimeInMs,     "Execution time (ms)",  "", location.top)
plotchar(barsTimed,         "Bars timed",           "", location.top)
plotchar(barsNotTimed,      "Bars not timed",       "", location.top)