Anyone know any improvements for this data saving script?

I made this nice looking module that manages/creates stats for the player. It’s not completely finished yet, but the system is pretty much done. What I want to know is if I’ve done the best I possibly could at making sure my code is organized, and functions efficiently.

local module = {}

local Data = game:GetService("DataStoreService")
local PlayerData = Data:GetDataStore("StatData")
local Http = game:GetService("HttpService")

local GlobalData = {}
local PlayerStats = {
	Cash = 0;
	Muscle = 0;
	MuscleExp = 0;
	Inventory = "";
}

function AddAttributes(Player, Table) -- Creates attributes + values from the table above, or from data.
	print(Table)
	for AttributeName, AttributeValue in pairs(PlayerStats) do
		print(AttributeName, AttributeValue)
		if Table then
			AttributeValue = Table[AttributeName] or AttributeValue
		end
		Player:SetAttribute(AttributeName, AttributeValue)
		GlobalData[Player.UserId][AttributeName]=AttributeValue
		print(GlobalData[Player.UserId][AttributeName])
	end
end
function LoadStats(Player) -- Adds player to GlobalData table. (no remove part yet)
	if PlayerData:GetAsync(Player.UserId .. " Stats") then
		local DataFromPlayer = PlayerData:GetAsync(Player.UserId .. " Stats")
		GlobalData[Player.UserId] = DataFromPlayer
	else
		GlobalData[Player.UserId] = {}
	end
	AddAttributes(Player, GlobalData[Player.UserId])
end
game.Players.PlayerAdded:Connect(LoadStats)

local Auto_Save = coroutine.create(function() -- Autosave function (saves whole table as string)
	while wait(30*#game.Players:GetPlayers()) do
		for UserId,_ in pairs(game.Players:GetPlayers()) do
			local Player = game.Players:GetPlayerByUserId(UserId)
			if Player then
				local success, fail = pcall(function()
					PlayerData:SetAsync(Player.UserId .. " Stats", Http:JSONEncode(GlobalData[Player.UserId]))
				end)
				if fail then
					print(fail)
				else
					print("Saved " .. tostring(Player) .. "'s Data!")
				end
			end
		end
	end
end)
coroutine.resume(Auto_Save)

return module
3 Likes

From what I’ve read, it looks very neat & tidy, I’d say it’s good from my view.

2 Likes

Hi! Like @FeudalLucy already said, you script is indeed pretty neat, so there are only a couple of minor changes to be done.

Firstly, this doesn’t affect your code at all, but you should name your module, for example, rename it to “DataManager” or something descriptive. Everything great, but there are some little changes for better to be done in Auto_Save coroutine.

  1. Use UpdateAsync() instead of SetAsync(). It helps you carefully update the previous data.

(image from Roblox Dev Hub | Data Stores)

  1. If I were you, I’d replace
while wait(time here) do

if favour of

while (true) do
-- wait here
end

To my knowledge wait() isn’t guaranteed to run reliably, so second option should be a way to go.

  1. Lastly, use game:GetService(“Players”) to find services. I also noticed the following line:
for UserId,_ in pairs(game.Players:GetPlayers()) do
	local Player = game.Players:GetPlayerByUserId(UserId) -- not needed
	-- further code
end

which is not really needed, because

for _, player in pairs(game:GetService("Players"):GetPlayers()) do
-->> player value provides you the player object already
end

Lastly, you should probably add Players to the services list you have defined at the top.

Otherwise, again, you script looks quite neat, so I don’t currently see any other changes you should be doing.

Good luck with your project!

EDIT (2021-03-22)

UpdateAsync() works pretty well and is very welcome, but you can of course use SetAsync() for certain data types. It ultimately depends on what data you are saving. I found this useful reply by @rogchamp.

Take look at the important extension of this post!

2 Likes

Don’t do that, it’s a trap!

The answer was SaveAsync()

1 Like

I recommend auto-saving data every 30 seconds, there is no point in auto-saving data for every player after such a long wait. SetAsync yields the thread, therefore you need to save it in a new thread for every player so that you’re saving data for every player without waiting for one to finish and there is no need to encode the data into a JSON format since Roblox does that automatically:

while true do
     for _, player in ipairs(game.Players:GetPlayers()) do
        local success, fail = pcall(function()
			coroutine.wrap(PlayerData.SetAsync)(PlayerData, Player.UserId .. " Stats", GlobalData[player.UserId])
		end)

		if fail then
			 print(fail)
		 else
			 print("Saved " .. Player .. "'s Data!")
		 end
     end
     wait(30)
end

You probably mean to use the default data PlayerStats instead of a empty table:

	if PlayerData:GetAsync(Player.UserId .. " Stats") then
		local DataFromPlayer = PlayerData:GetAsync(Player.UserId .. " Stats")
		GlobalData[Player.UserId] = DataFromPlayer
	else
		GlobalData[Player.UserId] = {} -- Deep copy Player Stats
	end

You should also retry if the pcall fails, save data whenever a player leaves and also on server shutdown.

@EssenceExplorer

  1. To my knowledge wait() isn’t guaranteed to run reliably, so second option should be a way to go.

No, the reason for that type of while loop idiom is because you aren’t relying on wait to return a truthy value.

  1. Use UpdateAsync() instead of SetAsync(). It helps you carefully update the previous data.

Yes, but note that UpdateAsync isn’t necessarily used to check the old data and carefully update it. It’s actual use-case is cancelling the save request by returning nil and it respects incoming calls and queries them.

1 Like

@SilentsReplacement you are right, data is formated in JSON format as part of saving process automatically, so we surely don’t need to do that. Thank you for commenting my wait() suggestion too. It was more of an extra tip, although using while true do loops is generally speaking considered a better practice for the reasons explained in the following post:

Secondly, I agree that saving time interval is not rapid enough. I accidentally missed this part:

And misread * operator. Albeit + is a better option in this same loop, saving every 30 seconds doesn’t hurt.

while (true) do
	wait(30)
	-- or
	wait(30 + Players:GetPlayers())
end

Lastly, I see your point. Wrapping SetAsync() function in coroutines for each player seems like a solid idea to synchronize data saving, but I’m a little concerned about this method. Having too many threads, for example 30 in this case, usually indicates that we should find a better solution. Albeit such number of threads is not an issue when it comes to performance (unless functions are carrying out some demanding tasks), the problem might be DataStore and network limit. We can only send so many requests at the time, meaning exceeding the limit results in requests queuing.

It may not happen in this case, but it’s definitely wise to update data player by player, and wait for a second or so between requests. During the game session, there is no reason really to update on time, but each time period instead, and when player leaves.

Have a great day!

You can have over 250 threads running at the same time with no problem at all, from what I’ve tested. It may be not so efficient, but its the only effective way to save data before server shutdown.

EDIT (2021-03-23)

This might be an issue. What if you have 30 or 50 players?

Good point, but server won’t shut down until script finishes. A better approach would perhaps involve making a table on the server and inserting data into it for each player, each time we decide to save data. When server is about to shut down, we should instead save data into that table and then send contents of that table to DataStore, drastically reducing the possiblity of player leaving an not having their data saved (brainstorming).

(Changed the following part a little bit.)
@Wyzloc you may want to create a big table, and perhaps update it with new player’s data on each auto-save. Before server shuts down if data writing fails, take the data from that table you have saved, and send it’s content to DataStore. It’s won’t matter whether players leave the game in the meantime.

Alternatively, you could make one separate key for all most important data to temporary save it just in case.

Good point, but server won’t shut down until script finishes. A better approach would be making a table on the server and inserting data into it for each player, each time we decide to save data. When server is about to shut down, we should instead save data into that table and then send contents of that table to DataStore, drastically reducing the possiblity of player leaving an not having their data saved.

This is incorrect, the server will shutdown despite if the script finishes after 30 seconds, that’s why saving data in a new thread for every player is heavily needed.

There is also need to insert data for each player, since of course you won’t be calling GetAsync or UpdateAsync to retrieve data but to save that cached data. There are often times where the player rejoins another server before their data is saved on another server, to counter that, you would need to implement session locking.

this might be an issue. What if you have 30 or 50 players?

IIRC, the maximum players per server is around 200 and like I said, you can have over 250 threads running at the same time with no problem.

That’s not the point of what I’m saying. You can have 500 or even 1000 and more threads running. It doesn’t affect performance a lot, but that of course depends on what the function does. If tasks are heavy, having a lot of demanding threads of course does affect performance in a negative way.

I’m talking about DataStore limits. And again, there is a small chance write requests start queuing. I’ve done some tests and tried performing 220 requests simultaneously. The execution time was surprisingly small (10^(-3) s and less), but every third attempt errors raised because of frequent requests.

Now, I’m not saying this would happen in game, as most games don’t have more than 50 players in the server. However, creating coroutines during each auto-save isn’t needed. Web DataStore requests yield the code for about 0.2 s, which is arguably big time interval, although we can still rely on it during the game sessions. We don’t need auto-saving done exactly on time. On the other hand saving for the last time is different.

That’s when coroutines come in handy. There are rare occurances when we will be sending 150+ requests for different players in a short amount of time (only happens on sudden server shutdowns).

Since we mention close time here, I’d like to thank you for correcting me on server shutdown. Unless we somehow stall server closing time, which is a bad practice, server closes 30 seconds after BindToClose event.

(Changed the following paragraph slightly, because PlayerRemoving doesn’t run on server shutdown. Thanks to SilentsReplacement for that information.)

I believe PlayerRemoving is a better choice, because it runs before player leaves. This is when coroutines should be called. If @Wyzloc’s game had completely full servers with 200 players constantly, which is very unlikely for anyone, we would need to take further precautions, like add small waiting times to prevent potential request queues.

local Players = game:GetService("Players");

Players.PlayerRemoving:Connect(function(player)
	coroutine.wrap(function()
		-- Save data here.
	end)
end)

The script by original poster currently doesn’t contain any code blocks that take care of saving when player is removed from the game, and I can’t really know whether that’s delibereate or not.

EDIT @SilentsReplacement

I think we improved the code pretty well together. We’ve covered some important aspects, so I say this time was pretty productive and this thread is pretty comprehensive. It’s not about arguing or proving each other wrong, but discussing improvements. You’ve given some useful insight and I’m glad we’ve drawn some relative conclusions. :slight_smile:

That’s not the point of what I’m saying. You can have 500 or even 1000 and more threads running. It doesn’t affect performance a lot, but that of course depends on what the function does. If tasks are heavy, having a lot of demanding threads of course does affect performance in a negative way.

That isn’t my point either, my point is that you should save data in a new thread for every player on BindToClose.

I’ve done some tests and tried performing 220 requests simultaneously. The execution time was surprisingly small (10^(-3) s and less), but every third attempt errors raised because of frequent requests.

Your point is invalid here, and its impossible to have a data write request take less than a second, it makes sense and here are my benchmarks:

local DataStore = game:GetService("DataStoreService"):GetDataStore("Test")

local averageRequestTime = 100
local averageTime = 0

for i = 1, 100 do
	local timePassed = os.clock()
	
	pcall(function()
		DataStore:SetAsync(tostring(i), {})
	end)

	averageTime += os.clock() - timePassed
end

-- average time: 2 seconds for 100 threads

As you can clearly see, an average time a request takes is about 2 seconds, your benchmark was inaccurate, we’re talking about time it takes for an request to complete, not execution time of the script.

Now, I’m not saying this would happen in game, as most games don’t have more than 50 players in the server. However, creating coroutines during each auto-save isn’t needed. Web DataStore requests yield the code for about 0.2 s, which is arguably big time interval, although we can still rely on it during the game sessions. We don’t need auto-saving done exactly on time. On the other hand saving for the last time is different.

No, as I said, DataStore write requests take a minimum of 1 second and an average of 2-7 seconds, you should never rely on it as it isn’t very reliable and can also take more than 7 seconds, this isn’t rare and happens occasionally. This would be not-so good for a server with more than 15 players, and there is also a possibility that the data store request takes more than 7 seconds, especially when Roblox services are slow.

Assume that each write request takes about 2 seconds in a 32 player server, then it would take about 72 seconds to save data for every player in the game which isn’t possible since the server will forcefully shutdown after 30 seconds, this is where my point stands. You should be saving data in a new thread for every player on server shutdown.

However, we shouldn’t be using it. Instead, I believe PlayerRemoving is a better choice, because it runs before player leaves. This is when coroutines should be called. If @Wyzloc’s game had completely full servers with 200 players constantly, which is very unlikely for anyone, we would need to take further precautions, like add small waiting times to prevent potential request queues.

PlayerRemoving doesn’t get the chance to fire when the server is shutting down, this is why BindToClose exists, therefore your point isn’t valid here.

we would need to take further precautions, like add small waiting times to prevent potential request queues.

That isn’t the point of this topic, the user should handle edge cases them self. Adding small wait times won’t prevent potential request queues:

And that is the point I accepted. I didn’t mention it myself at the start, because the original poster didn’t have any data saving on shutdowns implemented.

However, it’s also on @Wyzloc to decide whether to create 30 coroutines each 30 seconds, or just save data less periodically and more gradually. If your servers run for hours, we aren’t losing anything by not using coroutines. Burden for the server is almost unnoticable in both cases, so it’s on individual to decide. The original way of looping is arguably completely fine, only a little less regular.

No, it’s not invalid. It had different purpose, which was showing execution time of the script, and not individual request time. What I was trying to show is that using coroutines, you can activate requests in such short time.

This is not entirely true, since what I said stands. I don’t know how exactly you measured average request time, but the final result is not 2 second, it’s 0.2 and less. It takes only around 7 seconds to save data of 32 players.

Please stop invalidating my points. Yes, you can use BindToClose, but there is absolutely no reason to not implement PlayerRemoving. BindToClose is only useful to implement as a precaution and for saving at server shutdown. That is if player shuts down the server through game settings, updates it, restarts it, soon after every player leaves, or some error occurs that closes the server.

Otherwise, PlayerRemoving is very effective. Server doesn’t close immediately when last player leaves. Try it yourself with the following code. It’s worth to mention that it only works in game environment, not in studio.

Test code
local DataStore = game:GetService("DataStoreService"):GetDataStore("Test")

local REQUEST_NUM = 50

local data = {}

local success, err = pcall(function()
	for i = 1, REQUEST_NUM do
		local n
		pcall(function()
			n = DataStore:GetAsync(i)
			DataStore:RemoveAsync(i)
		end)
		if (n ~= nil) then table.insert(data, n) end
	end
end)

print(#data, REQUEST_NUM)
print("Wait for a couple of seconds before leaving!")

game:GetService("Players").PlayerRemoving:Connect(function(player)
	for i = 1, REQUEST_NUM do
		pcall(function()
			DataStore:SetAsync(i, math.random(1,100))
		end)
	end
end)

I’m not talking about adding waiting times using wait(). Instead, I’m talking about another approach. For example (maybe there are better ways, but it’s not what we are looking for right now), we could track a number of running coroutines, and only spawn new ones when some of the previous ones have finished and when write request queue has been avoided.

This is indeed not the this topic’s discussion, and that’s why I only mentioned it, not further dived into it.

EDIT We obviously need both, BindToClose and PlayerRemoving for secure data saving.

Please stop invalidating my points. Yes, you can use BindToClose, but there is absolutely no reason to not implement PlayerRemoving

You stated to use PlayerRemoving over BindToClose which is definitely not a better choice as both are needed, I’m not “invalidating” your points.

However, we shouldn’t be using it. Instead, I believe PlayerRemoving is a better choice, because it runs before player leaves.

This is not entirely true, since what I said stands. I don’t know how exactly you measured average request time, but the final result is not 2 second, it’s 0.2 and less . It takes only around 7 seconds to save data of 32 players .

Respectfully, this is not true. An average request takes about 1-2 seconds minimum, not any lower than that. Think of it logically; if an average request took about 0.2 seconds, you wouldn’t notice any yielding in between and the word Async at the last of it wouldn’t suit it.

PS: I’ve concluded this is the end of this discussion, as we’re just cluttering up. Both of us provided sufficient points and solutions.

Yes, I corrected myself and edited the reply right after I posted. Both functions are needed for successful data saving, and we should use coroutines in both those cases.

(Only goes for casul saving.)

I don’t know how you got the average of 1-2 seconds, because my information is definitely correct. We have to calculate the average time for data saving, which is total time devided by number of requests. Prints and additional calculation should also be excluded, because they add up final time.

local DataStore = game:GetService("DataStoreService"):GetDataStore("Test")

local REQUEST_NUM = 50
local start, finish

start = os.clock()

for i = 1, REQUEST_NUM do
	pcall(function()
		DataStore:SetAsync(tostring(i), {})
	end)
end

finish = os.clock() - start
print(finish/REQUEST_NUM)

EDIT

Yielding code means stopping the whole script execution for a period of time, be it 0.03 seconds, which is approximately what wait() does, or 10 minutes. It is exactly what Async does, even though request time is 0.2 s.

EDIT (2)

No, that is not what average means.

Average is a single value (such as a mean, mode, or median) that summarizes or represents the general significance of a set of unequal values.
(Average Definition & Meaning - Merriam-Webster, 2021-22-03)

5 requests ==> =~ 1.7 s
1 request ==> =~ 0.34 s  ;  0.19 in my tests.

EDIT (3) @SilentsReplacement

This doesn’t give you the impression of yielding if you only perform one request, but does make a significant one if you call 5 or more requests, for example.

I don’t know how you got the average of 1-2 seconds, because my information is definitely correct. We have to calculate the average time for data saving, which is total time devided by number of requests. Prints and additional calculation should also be excluded, because they add up final time.

Your benchmark is clearly inaccurate, plus, there are no prints and additional calculation is extremely negligible. Your information is not correct at all.

An average request takes about a 1.7 seconds to complete, if that is consistent, then it would take about 1.7 * 50 seconds for 50 requests to complete in total.

local DataStore = game:GetService("DataStoreService"):GetDataStore("123")

local before = os.clock()
DataStore:SetAsync(123, {})
print(os.clock() - before) -- 3 seconds, not 1.7!!!!!
local DataStore = game:GetService("DataStoreService"):GetDataStore("123")

local before = 1.7 * 5
local finalTime = 0

for i = 1, 3 do
	local before1 = os.clock()
	DataStore:SetAsync(123, {})
	finalTime += os.clock() - before1
end

print(finalTime / before)

An average request takes 1.5 seconds to complete, but in the benchmark above this one, you can clearly see that a request took about 3 seconds to complete, therefore your point is incorrect.

local DataStore = game:GetService("DataStoreService"):GetDataStore("Test")

local REQUEST_NUM = 1
local start, finish

start = os.time()

for i = 1, REQUEST_NUM do
	pcall(function()
		DataStore:SetAsync(tostring(i), {})
	end)
end

finish = os.time() - start
print(finish)

This doesn’t give you the impression of yielding if you only perform one request, but does make a significant one if you call 5 or more requests, for example.

It does give you the impression of yielding, stop being so subjective, despite it being a second or not.

A single request takes about 1.7 seconds, not 0.34, your benchmark is clearly inaccurate. I don’t know how you want this elaborated to you.

EDIT:

No, that is not what average means.

No, you aren’t getting the average correctly, accept your flaw. There is no reason to argue pointlessly just because you’re proven wrong.

@SilentsReplacement I am getting tired of this. Before you make such bold accusations, please think about what you are posting! You have clearly deleted you previous post, and here you are now accusing me of my test being inaccurate, when you are neither doing objective tests nor substantiating them.

Objectiveness

You obviously took my words out of context. I said this in a subjective manner, but exclusively after I explained why we could get the impression of script not yielding.

My calculations are closer to reality than yours. They aren’t perfect of course, because I didn’t dive into covering all cases or took scientific approach, but they are informative enough and stand on solid grounds. Period.

Average

In your previous post, you weren’t properly calculating average. I even posted the definition of it.

How would you calculate the average? Let’s say you have the following measurements:

0.5, 0.2, 0.4, 0.8, 0.7, 0.1

How do we get the average number?

(0.5 + 0.2 + 0.4 + 0.8 + 0.7 + 0.1) / 6 = avg. --> sum of all devided by number of all

Which confirms my way of calculating was correct. It’s no rocket science.

Validity

Firstly, we can’t say any of our tests are technically completely valid, because we didn’t take the scientific approach.

However, let’s dive into our approaches a little bit.

Your code - one request - test 1

Code here

This code is the least accurate of all. Why? Because 1. it’s not average result; 2. it’s not measured under usual circumstances, which makes it deviate from average.

Such tests have no point, because the game is still starting, so most of the web requests you send are delayed significantly. This is one of the worst case scenarios, far from average. You would be closer to average if you added a wait statement at the end (e.g. wait(8) or wait(6) ).

You shouldn’t be doing tests in studio. Game environment (real Roblox server) is a better and more objective alternative. Even I get around 2 seconds for request when I click PlayTest, but that doesn’t mean I got the average or valid result. Roblox server in usual circumstances returns the requested value much earlier.

Your code - multiple requests; not average result - test 2

Code here

This is not at all average result.

local before = 1.7 * 5

You are deviding final time with this custom number. You are also adding up numbers throughout each loop, which is ultimately the same as getting finish time at the end of the script.

You code - os.time() - test 3

Code here

This test would show the real result if you used os.clock() instead of os.time(). The former only displays seconds, so it’s not at all precise enough for our tests.

Run the same code in os.clock() and observe the results (and perhaps add wait() statement).

Again, I’m not being subjective, but average of 0.2 indeed gives you an impression of script not yielding, if we only perform one request.

With everything said in this post, I think I argumented my point of view clearly enough. Elaborated? Nothing to elaborate here. Firstly, make sure you have a solid explanation of something.

It’s you not getting average, and I don’t apprecate such active aggresiveness. We are not here to argue, and it’s certainly not why I responded to @Wyzloc’s post. I’m here to help and give insight.

“No reason to argue pointlessly just because you’re proven wrong.”

See, the thing is that this statement should be directed towards you, because I haven’t been proven wrong and we are not even here to prove each other wrong. What you are doing is provoking and trying to prove me wrong, which doesn’t really contribute to this topic, which is the initial goal.

No saying that I didn’t give any at least partially false information. It’s not true that server closes after the main scripts finish and data is saved, but after around 30 seconds (BindToClose). However, I am pretty sure anything else I said is correct. I’ve pointed out that wrapping in coroutines is not necessary during the game, but it is sort of necessary on server closing and player leaving event.

Everyone has their flaws, but my test is pretty accurate.

Stop acting so aggresively.

One of the best tests we can perform

Now, here is a realtively valid test, which is similar to my previous one, but also displays lowest request time.

local DataStore = game:GetService("DataStoreService"):GetDataStore("Test")

local REQUEST_NUM = 50 -- Set number of requests here.

local elapsedTimes = {}
local start_1, start_2, average; lowest = math.huge

start_1 = os.clock()

for i = 1, REQUEST_NUM do
	start_2 = os.clock()
	pcall(function()
		DataStore:SetAsync(i, math.random(1, 100))
	end)
	lowest = math.min(lowest, os.clock() - start_2)
end
average = (os.clock() - start_1)/REQUEST_NUM
--- Disclaimer: 

print("Average request time (from server start): ".. math.floor(average *10e4)/10e4 .." seconds")
print("Lowest request time: ".. math.floor(lowest *10e4)/10e4 .." seconds")

EDIT (15 min later)

I’ve replaced my test with a little shorter version. The previous version produces the exact same type of result, but is longer.

Previous code
local DataStore = game:GetService("DataStoreService"):GetDataStore("Test")

local REQUEST_NUM = 50 -- Set number of requests here.

local elapsedTimes = {}
local start_1, start_2, average, lowest

start_1 = os.clock()

for i = 1, REQUEST_NUM do
	start_2 = os.clock()
	pcall(function()
		DataStore:SetAsync(i, math.random(1, 100))
	end)
	table.insert(elapsedTimes, os.clock() - start_2)
end

average = (os.clock() - start_1)/REQUEST_NUM

lowest = math.huge
for i, v in pairs(elapsedTimes) do
	if (v < lowest) then
		lowest = v
	end
end
--- Disclaimer: 

print("Average request time (from server start): ".. math.floor(average *10e4)/10e4 .." seconds")
print("Lowest request time: ".. math.floor(lowest *10e4)/10e4 .." seconds")

I deleted my old post because I was tired of editing it since you were editing your proven wrong points, I’m not making any bold accusations, you’re the one clearly making.

In your previous post, you weren’t properly calculating average. I even posted the definition of it. How would you calculate the average? Let’s say you have the following measurements:

Why are you blatantly lying that I calculated the average incorrectly? Instead of taking average time, first check how long an request takes:

local DataStore = game:GetService("DataStoreService"):GetDataStore("123")

local a = os.clock()	
DataStore:SetAsync(1, {})
print(os.clock() - a) -- 1.9 secs

If this time is consistent, then it would take about 1.9 * 10 seconds for 10 requests to complete.

This test would show the real result if you used os.clock() instead of os.time() . The former only displays seconds , so it’s not at all precise enough for our tests.

I’d like to mention that os.clock is only so accurate in milliseconds, the result is an rough estimate but not inaccurate.

See, the thing is that this statement should be directed towards you, because I haven’t been proven wrong and we are not even here to prove each other wrong . What you are doing is provoking and trying to prove me wrong , which doesn’t really contribute to this topic, which is the initial goal.

I’m not, its just you thinking that I’m and this is off topic.

Everyone has their flaws, but my test is pretty accurate.

Taking average isn’t a good idea, instead take an estimate on how long a request takes to complete and conclude.

My benchmark once again is accurate, average is just accumulated / total, which is what my benchmark is doing, it is assuming that an request normally takes 1.7 seconds to complete.

local DataStore = game:GetService("DataStoreService"):GetDataStore("123")

local before = 1.7 * 5
local finalTime = 0

for i = 1, 3 do
	local before1 = os.clock()
	DataStore:SetAsync(123, {})
	finalTime += os.clock() - before1
end

print(finalTime / before)

An average request takes about 1.6 seconds to complete (tested from my benchmark), therefore it means that it would take about 1.6 * 10 seconds (estimate) for 10 requests to complete.

EDIT:

I noticed that there is a bug, saving for the same key in less than 6 seconds for some reason didn’t throttle, and saving only takes about 0,3 seconds exact for next keys. Which is why you’re getting these results.

Keep in mind that DataStore write requests will never take less than a second to complete.

@SilentsReplacement I am going to end this discussion. You didn’t even properly read my post, in which I tried to politely explain why your benchmark doesn’t display results accurately enough. You are calculating “average” at the start of the game, when server is starting and delays are significantly bigger. Like I already said, try getting results throughout the game and see the difference.

You are now saying that calculating average doesn’t work, and that’s the problem. If you measure swimming time from one side of the pool to the other, would you get accurate speed if you only measured swimmers speed after they start and push themselves from the edge of the pool? Obviously not.

I find it insulting that you are writing posts minutes after I posted mine, themselves indicating that you didn’t even read mine, into which I put effort to explain different aspects.

Roblox services are not as slow as you believe, and if you did relatively valid tests, you would realize that yourself.

I have done some mistakes and they are part of my learning journey, but I strongly believe that I have given some reasonable insight into this topic and posted relatively big amounts of correct information. I’ve argumented my statements with repeatable tests.

You are getting off topic and dragging me off topic too. Saying that my tests aren’t valid when you have arguments confirming otherwise doesn’t make sense.

You can always save different data under different keys without deliberate restrictions. Data saves successfuly.

Requests and data can be sent quickly, but actual saving takes more time, although it doesn’t seem to affect the script.

EDIT

image

It is much more suitable choice than os.time().

Requests and data can be sent quickly, but actual saving takes more time, although it doesn’t seem to affect the script.

I agree but this must come with a citation:

I noticed that there is a bug, saving for the same key in less than 6 seconds for some reason didn’t throttle, and saving only takes about 0,3 seconds exact for next keys. Which is why you’re getting these results.

Keep in mind that DataStore write requests will never take less than a second to complete.

The least amount of time a single data store request ever took was 0.9 seconds.

You can always save different data under different keys without deliberate restrictions. Data saves successfuly.

EDIT

It is much more suitable choice than os.time().

This is correct, however I never said that os.time is much suitable, so need to make pointless points.

I find it insulting that you are writing posts minutes after I posted mine, themselves indicating that you didn’t even read mine, into which I put effort to explain different aspects.

I read all of your posts, this is subjective.

You are calculating “average” at the start of the game, when server is starting and delays are significantly bigger. Like I already said, try getting results throughout the game and see the difference.

Yes, testing after 5 seconds still yields the same result:

local DataStore = game:GetService("DataStoreService"):GetDataStore("123")

local before = 1.7 * 5
local finalTime = 0

wait(5)

for i = 1, 3 do
	local before1 = os.clock()
	DataStore:SetAsync(123, {})
	finalTime += os.clock() - before1
end

print(finalTime / before)

You are now saying that calculating average doesn’t work, and that’s the problem. If you measure swimming time from one side of the pool to the other, would you get accurate speed if you only measured swimmers speed after they start and push themselves from the edge of the pool? Obviously not.

I noticed that there is a bug, saving for the same key in less than 6 seconds for some reason didn’t throttle, and saving only takes about 0,3 seconds exact for next keys. Which is why you’re getting these results.

Keep in mind that DataStore write requests will never take less than a second to complete.

I’ve concluded the end of this discussion, as we both are just cluttering this up for no reason, your benchmark is incorrect; accept it, no need to argue pointlessly.

What you are asking me to do is accept something unargumented, not supported by beliveable and sufficient information. I’ve explained in this post why you shouldn’t rely on results from your tests. I don’t like your agressiveness and ignorant behaviour, and you are not the one to call posts pointless, especially without any valid counter-arguments. You just keep pasting the same function over and over, which contains calculations that don’t seem to by related.

local DataStore = game:GetService("DataStoreService"):GetDataStore("123")

local before = 1.7 * 5
local finalTime = 0

wait(5)

-- You aren't performing enough requests (3 is not enough).
for i = 1, 3 do
	-- PREVIOUS ---------------------------------------------
	local before1 = os.clock()
	-- Saving to same key results in DataStore throttling.
	DataStore:SetAsync(123, {})
	finalTime += os.clock() - before1
	---------------------------------------------------------
	
	-- DO THIS INSTEAD --------------------------------------
	local before1 = os.clock()
	pcall(function()
		DataStore:SetAsync(i, {})
	end)
	finalTime += os.clock() - before1
	---------------------------------------------------------
end

--[[
	You are deviding final time with some
	number you defined.
]]
print(finalTime / before)
-- Correct:
print(finalTime /3)

Again, all the tests I’m doing and explain indicate otherwise. Data saves successfully, you are claiming otherwise and keep repeating that without further elaborating. Such saving is rather expected behaviour than a bug. You can’t save data to the same key under 6 seconds (usually), but there are no such limits to saving data under different keys.

I’ve ran your function with some modifications pasted above, and I get similar results to mine.

Lastly, I mentioned os.clock(), because you yourself are using os.time(). time() is far better, but the best option is os.clock().

You are acting very agressively and are driven by inflated ego and vindictiveness. An indicator of that is the comment you just posted in response to my other posts in some other topic. You comment is accusing me of spreading false information, which are not false, at least not according to this famous post on Stack Overflow: Concatenation of strings in Lua - Stack Overflow.

At the time of writing that post, I wasn’t completely aware that micro-optimization is not to be discussed, but I kept the post, because it’s informing and has higher purpose than just discussing micro-optimization.

https://devforum.roblox.com/t/timespanparse-function/1088909/15?u=essenceexplorer

You went straight to my profile to find some topic where you could sully me, and pasted accusing reply without even reading through. As you could see if you bothered to read other replies, you would realize that you post is relatively off topic.

I am now calling moderators for review. For some reason you like to argue, which is not welcome here. I’ve noticed you argue this way elsewhere as well. @Moxyll I don’t know who to mention right now, and I thank you in advance for your response. I will delete this post if necessary, although it seems reasonable to leave it here for now.

EDIT

If you read the topic, which you clearly didn’t:

local string1 = "A"

string1 = string1 .. "B" .. "C" ---> creates one string

local t = {"A", "B", "C"}
string1 = ""
for i, v in pairs(t) do
	string1 = string1 .. v ---> results in a lot of strings
end