Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvement for asXXX functions #51

Open
TwitchBronBron opened this issue May 17, 2022 · 1 comment
Open

Performance improvement for asXXX functions #51

TwitchBronBron opened this issue May 17, 2022 · 1 comment
Labels
wontfix This will not be worked on

Comments

@TwitchBronBron
Copy link
Collaborator

I noticed that the asXXX functions transpile the key to a single string. That means you have to do runtime logic to split the keys. Wouldn't it be better to pre-split them at compile-time to save some cycles?

Proposal

Source:

print getBoolean(json.user.favorites[0].isActive)

Transpiled (current):

print mc_getBoolean(json, "user.favorites.0.isActive")

Transpiled (proposed):

print mc_getBoolean(json, ["user", "favorites", "0", "isActive"])

Justification

Here are some benchmark results, which show 12.9% speed improvement:

string split: ---- 27,578.00 ops/sec (slowest)
already split: --- 31,142.00 ops/sec (fastest)

Here are the benchmarks in case you'd like to run them yourself.

sub main()
    runAllTests()
end sub

sub runAllTests()

    print "Tests starting"
    m.opCount = 100000
    m.testResults = []

    stringVsArrayKeyLookups()

    printResults(m.testResults)

    startTime = CreateObject("roDateTime")
    'the print buffer isn't flushed when the app dies, so spin till it flushes
    print " "
    while CreateObject("roDateTime").AsSeconds() - startTime.AsSeconds() < 1
    end while
end sub

sub stringVsArrayKeyLookups()
    json = {
        user: {
            favorites: [
                {
                    isActive: true
                }
            ]
        }
    }
    runTest("string split", sub(opCount, json, path)
        getPath = function(content as object, path as string, default = invalid as dynamic, disableIndexing = false as boolean) as dynamic
            part = invalid

            if path <> invalid
                parts = path.split(".")
                numParts = parts.count()
                i = 0

                part = content
                while i < numParts and part <> invalid
                    if not disableIndexing and (parts[i] = "0" or (parts[i].toInt() <> 0 and parts[i].toInt().toStr() = parts[i]))
                        if type(part) <> "<uninitialized>" and part <> invalid and GetInterface(part, "ifArray") <> invalid
                            part = part[parts[i].toInt()]
                        else if type(part) <> "<uninitialized>" and part <> invalid and GetInterface(part, "ifAssociativeArray") <> invalid
                            part = part[parts[i]]
                        else if type(part) = "roSGNode"
                            part = part.getChild(parts[i].toInt())
                        else
                            part = invalid
                        end if
                    else
                        if type(part) <> "<uninitialized>" and part <> invalid and GetInterface(part, "ifAssociativeArray") <> invalid
                            part = part[parts[i]]
                        else
                            part = invalid
                        end if
                    end if
                    i++
                end while
            end if

            if part <> invalid
                return part
            else
                return default
            end if
        end function

        for i = 0 to opCount
            getPath(json, path)
        end for
    end sub, json, "user.favorites.0.isActive")

    runTest("already split", sub(opCount, json, keys)
        getPath = function(content as object, parts, default = invalid as dynamic, disableIndexing = false as boolean) as dynamic
            part = invalid

            if parts <> invalid
                numParts = parts.count()
                i = 0

                part = content
                while i < numParts and part <> invalid
                    if not disableIndexing and (parts[i] = "0" or (parts[i].toInt() <> 0 and parts[i].toInt().toStr() = parts[i]))
                        if type(part) <> "<uninitialized>" and part <> invalid and GetInterface(part, "ifArray") <> invalid
                            part = part[parts[i].toInt()]
                        else if type(part) <> "<uninitialized>" and part <> invalid and GetInterface(part, "ifAssociativeArray") <> invalid
                            part = part[parts[i]]
                        else if type(part) = "roSGNode"
                            part = part.getChild(parts[i].toInt())
                        else
                            part = invalid
                        end if
                    else
                        if type(part) <> "<uninitialized>" and part <> invalid and GetInterface(part, "ifAssociativeArray") <> invalid
                            part = part[parts[i]]
                        else
                            part = invalid
                        end if
                    end if
                    i++
                end while
            end if

            if part <> invalid
                return part
            else
                return default
            end if
        end function
        for i = 0 to opCount
            getPath(json, keys)
        end for
    end sub, json, ["user", "favorites", "0", "isActive"])
end sub

function getOpsPerSec(startDate, endDate, ops)
    startMs = getMilliseconds(startDate)
    endMs = getMilliseconds(endDate)
    seconds = (endMs - startMs) / 1000
    opsPerSec = ops / seconds
    return opsPerSec
end function

function getMilliseconds(date)
    result = 0
    result += date.GetMinutes() * 60 * 1000
    result += date.GetSeconds() * 1000
    result += date.GetMilliseconds()
    return result
end function

function numberToString(num)
    result = ""
    i = 0
    while num > 1
        loopNum = (num mod 10).ToStr().Trim()
        result = loopNum + result
        num = num / 10
        i++
        if i mod 3 = 0 and num > 1 then
            result = "," + result
        end if
    end while
    result = result + "." + ((num * 10) mod 1).ToStr().Trim() + ((num * 100) mod 1).ToStr().Trim()
    return result
end function

'
' Run a single test.
' @param name - the name of the test
' @param testFn - reference to the test to run
' @param args - an array of parameters to pass as arguments to the test function
'
sub runTest(name as string, testFunc as function, arg1 = invalid, arg2 = invalid, arg3 = invalid, arg4 = invalid)
    opCount = m.opCount
    if arg4 <> invalid
        startTime = CreateObject("roDateTime")
        testFunc(opCount, arg1, arg2, arg3, arg4)
        endTime = CreateObject("roDateTime")
    else if arg3 <> invalid
        startTime = CreateObject("roDateTime")
        testFunc(opCount, arg1, arg2, arg3)
        endTime = CreateObject("roDateTime")
    else if arg2 <> invalid
        startTime = CreateObject("roDateTime")
        testFunc(opCount, arg1, arg2)
        endTime = CreateObject("roDateTime")
    else if arg1 <> invalid
        startTime = CreateObject("roDateTime")
        testFunc(opCount, arg1)
        endTime = CreateObject("roDateTime")
    else
        startTime = CreateObject("roDateTime")
        testFunc(opCount)
        endTime = CreateObject("roDateTime")
    end if
    result = {
        name: name
        opsPerSec: getOpsPerSec(startTime, endTime, opCount)
    }
    print name; " (DONE)"
    m.testResults.push(result)
end sub

function printResults(results)
    print ""
    print "RESULTS:"
    print padRight("", 50, "_")
    print ""
    highestOpsPerSec = results[0].opsPerSec
    lowestOpsPerSec = results[0].opsPerSec
    opsPerSecMaxLen = 0
    nameLengthMaxLen = 0
    for each result in results
        'calculate slowest
        if result.opsPerSec < lowestOpsPerSec
            lowestOpsPerSec = result.opsPerSec
        end if

        'calculate highest ops/sec
        if result.opsPerSec > highestOpsPerSec
            highestOpsPerSec = result.opsPerSec
        end if

        'calculate logest ops/sec string
        opsPerSecTextLength = numberToString(result.opsPerSec).Len()
        if opsPerSecTextLength > opsPerSecMaxLen
            opsPerSecMaxLen = opsPerSecTextLength
        end if

        'calculate longest name
        if result.name.Len() > nameLengthMaxLen
            nameLengthMaxLen = result.name.Len()
        end if

    end for
    for each result in results
        postfix = ""
        if result.opsPerSec = highestOpsPerSec then
            postfix = " (fastest)"
        end if
        if result.opsPerSec = lowestOpsPerSec then
            postfix = " (slowest)"
        end if
        printResult(result, nameLengthMaxLen + 5, opsPerSecMaxLen, postfix)
    end for
end function

'
' Print a single test result
'
sub printResult(result, namePadding = 1, opsPadding = 0, postfix = "")
    print padRight(result.name + ": ", namePadding, "-"); " "; padLeft(numberToString(result.opsPerSec), opsPadding, " "); " ops/sec"; postfix
end sub

function padRight(value as string, padLength = 2 as integer, paddingCharacter = "0" as dynamic) as string
    while value.len() < padLength
        value += paddingCharacter
    end while
    return value
end function

function padLeft(value as string, padLength = 2 as integer, paddingCharacter = "0" as dynamic) as string
    while value.len() < padLength
        value = paddingCharacter + value
    end while
    return value
end function

sub noop()
end sub
@georgejecook
Copy link
Owner

georgejecook commented May 21, 2022

thanks - this is a good suggestion, which I'll definitely accept a pr on, if somebody needs it.

For me though, on my projects, there's no need.

The reason I've never sweated these getXXX methods, is coz I discourage parsing anything in my projects, as my mentor taught me when I first started doing roku.

So - while most people do 1000's or 10'000's of ops when they receive things from the wire, managing/parsing json fields, I do zero: I just bundle up the entire json in a node, and off it goes.

The renderers then end up doing the work - so the only part of any of my apps that can be using these asXXX methods, in any hot loop is a cell - and there's never more than say.. 200 getting rendered at a time. Given I use maestro list for everything these days, that's like.. < 50 max, at any given time.. (it supports rendering itself in batches, and is highly efficient to keep render thread nice and smooth).

In NBA, the most complex cell that I have, has 15 of these getXXX methods in it - we render 20 at at time, max.. so that means I'm going to save 1.4 ms of processing, in my worst case scenario

(rough figures: 12.9% perf increase on... 10.87ms.. (1000/27,578.00) *(20*15))

so, yeah - totally happy if someone who wants to do loads of pointless processing and uses maestro, wishes to improve their slow code.. they can give me a pr; but I need to go saving a lot more than 1.4ms before I start optimizing :)

@georgejecook georgejecook added the wontfix This will not be worked on label Feb 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wontfix This will not be worked on
Projects
None yet
Development

No branches or pull requests

2 participants