perf: optimize mapped, add benches

This commit is contained in:
littensy 2025-05-06 16:27:06 -07:00
parent d56672384a
commit ada1b80b87
12 changed files with 385 additions and 161 deletions

48
benchmarks/bench.luau Normal file
View file

@ -0,0 +1,48 @@
local profiles: { [string]: number } = {}
local times: { [string]: number } = {}
local stack: { string } = {}
local function run(name: string, test: () -> ())
local start = os.clock()
local iter = 0
table.clear(profiles)
table.clear(times)
table.clear(stack)
while os.clock() - start < 1 do
iter += 1
test()
end
local elapsed = os.clock() - start
local results = {}
table.insert(results, `{name}: \t{string.format("%.2f", elapsed / iter * 1000 * 1000)} us/iter`)
for name, elapsed in times do
table.insert(results, `{name}: \t{string.format("%.2f", elapsed / iter * 1000 * 1000)} us/iter`)
end
print(table.concat(results, " | "))
end
local function profilebegin(name: string)
profiles[name] = os.clock()
table.insert(stack, name)
end
local function profileend()
local name = table.remove(stack)
if name then
times[name] = (times[name] or 0) + os.clock() - profiles[name]
profiles[name] = nil
end
end
return {
run = run,
profilebegin = profilebegin,
profileend = profileend,
}

View file

@ -0,0 +1,99 @@
--!optimize 2
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Charm = require(ReplicatedStorage.Packages.Charm)
local atom = Charm.atom
local computed = Charm.computed
local mapped = Charm.mapped
local pauseTracking = Charm.signals.pauseTracking
local resumeTracking = Charm.signals.resumeTracking
type Entry = { number }
local count = 512
local initialState: { Entry } = table.create(count)
for i = 1, count do
initialState[i] = table.create(32, i)
end
local function mapper(value: Entry): Entry
return table.clone(value)
end
local function simpleMapped(source: () -> { Entry }): () -> { Entry }
return computed(function()
local state = source()
local nextState = {}
for index, value in state do
pauseTracking()
local success, valueOut = pcall(mapper, value)
resumeTracking()
if success then
nextState[index] = valueOut
end
end
return state
end)
end
local function updatePercentage(percent: number): ({ Entry }) -> { Entry }
return function(state)
local nextState = table.clone(state)
for index, value in state do
if math.random() < percent then
nextState[index] = table.clone(value)
end
end
return nextState
end
end
local function test(
percent: number,
profiler: any,
mapped: (src: () -> { Entry }, ...(Entry) -> Entry) -> () -> { Entry }
)
local src = atom(initialState)
local out = mapped(src, mapper)
profiler.Begin("init")
out()
profiler.End()
profiler.Begin("setup")
src(updatePercentage(percent))
profiler.End()
profiler.Begin("update")
out()
profiler.End()
end
return {
Functions = {
["mapped 10%"] = function(profiler)
test(0.1, profiler, mapped)
end,
["mapped 50%"] = function(profiler)
test(0.5, profiler, mapped)
end,
["mapped 100%"] = function(profiler)
test(1, profiler, mapped)
end,
["control 10%"] = function(profiler)
test(0.1, profiler, simpleMapped)
end,
["control 50%"] = function(profiler)
test(0.5, profiler, simpleMapped)
end,
["control 100%"] = function(profiler)
test(1, profiler, simpleMapped)
end,
},
}

80
benchmarks/mapped.luau Normal file
View file

@ -0,0 +1,80 @@
local Charm = require("@pkgs/charm/src/lib")
local bench = require("./bench")
local atom = Charm.atom
local computed = Charm.computed
local mapped = Charm.mapped
local pauseTracking = Charm.signals.pauseTracking
local resumeTracking = Charm.signals.resumeTracking
type Entry = { number }
local count = 512
local initialState: { Entry } = table.create(count)
for i = 1, count do
initialState[i] = table.create(64, i)
end
local function mapper(value: Entry, key: number): (Entry, number)
return table.clone(value), key + 1
end
local function simpleMapped(source: () -> { Entry }, mapper: (Entry, number) -> (Entry, number)): () -> { Entry }
return computed(function()
local state = source()
local nextState = {}
for index, value in state do
pauseTracking()
local success, valueOut, keyOut = pcall(mapper, value, index)
resumeTracking()
if success then
nextState[keyOut] = valueOut
end
end
return state
end)
end
local function updatePercentage(percent: number): ({ Entry }) -> { Entry }
return function(state)
local nextState = table.clone(state)
for index, value in state do
if math.random() < percent then
nextState[index] = table.clone(value)
end
end
return nextState
end
end
for _, percent in { 0.1, 0.5, 1 } do
bench.run(`mapped {percent}`, function()
local src = atom(initialState)
local out = mapped(src, mapper)
bench.profilebegin("init")
out()
bench.profileend()
src(updatePercentage(percent))
bench.profilebegin("update")
out()
bench.profileend()
end)
bench.run(`control {percent}`, function()
local src = atom(initialState)
local out = simpleMapped(src, mapper)
bench.profilebegin("init")
out()
bench.profileend()
src(updatePercentage(percent))
bench.profilebegin("update")
out()
bench.profileend()
end)
end

View file

@ -0,0 +1,54 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CharmSync = require(ReplicatedStorage.Packages.CharmSync)
local patch = CharmSync.patch
local USERS = 1000
local function generate()
local users = {}
for user = 1, USERS do
users[user] = {
coins = math.random(1, 2),
gems = math.random(1, 2),
}
end
return { users = users }
end
local function update(state)
state = table.clone(state)
state.users = table.clone(state.users)
for user = 1, USERS do
local rng = math.random()
if rng < 1 / 3 then
continue
elseif rng < 2 / 3 then
state.users[user] = nil
else
state.users[user] = {
coins = math.random(1, 2),
gems = math.random(1, 2),
}
end
end
return state
end
return {
ParameterGenerator = function()
local state = generate()
return state, update(state)
end,
Functions = {
diff = function(_, a, b)
patch.diff(a, b)
end,
},
}

View file

@ -0,0 +1,63 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Charm = require(ReplicatedStorage.Packages.Charm)
local atom = Charm.atom
local computed = Charm.computed
local effect = Charm.effect
local listen = Charm.listen
local function propagate(w: number, h: number)
local src = atom(1)
for _ = 1, w do
local last = src
for _ = 1, h do
local prev = last
last = computed(function()
return prev() + 1
end)
end
effect(function()
last()
end)
end
src(src() + 1)
end
local function propagateListen(w: number, h: number)
local src = atom(1)
for _ = 1, w do
local last = src
for _ = 1, h do
local prev = last
last = computed(function()
return prev() + 1
end)
end
listen(last, function() end)
end
src(src() + 1)
end
return {
Functions = {
["effect 1*100"] = function()
propagate(1, 100)
end,
["effect 100*1"] = function()
propagate(100, 1)
end,
["effect 10*10"] = function()
propagate(10, 10)
end,
["listen 1*100"] = function()
propagateListen(1, 100)
end,
["listen 100*1"] = function()
propagateListen(100, 1)
end,
["listen 10*10"] = function()
propagateListen(10, 10)
end,
},
}

27
benchmarks/propagate.luau Normal file
View file

@ -0,0 +1,27 @@
local Charm = require("@pkgs/charm/src/lib")
local bench = require("./bench")
local atom = Charm.atom
local computed = Charm.computed
local effect = Charm.effect
for _, w in { 1, 10, 100 } do
for _, h in { 1, 10, 100 } do
bench.run(`propagate {w}*{h}`, function()
local src = atom(1)
for _ = 1, w do
local last = src
for _ = 1, h do
local prev = last
last = computed(function()
return prev() + 1
end)
end
effect(function()
last()
end)
end
src(src() + 1)
end)
end
end