Async Support

Easy Save 2 has been replaced by Easy Save 3, so is no longer supported.
viveleroi
Posts: 17
Joined: Fri Feb 03, 2017 1:40 am

Async Support

Post by viveleroi »

Async options for writing to disk. After generating procedural terrain I save the data to disk but as the user walks around this causes a bit of lag. I'd rather write the data async because there's no need to wait for the result.
User avatar
Joel
Moodkie Staff
Posts: 4852
Joined: Wed Nov 07, 2012 10:32 pm

Re: Request a Feature (Post your Requests Here)

Post by Joel »

viveleroi wrote:Async options for writing to disk. After generating procedural terrain I save the data to disk but as the user walks around this causes a bit of lag. I'd rather write the data async because there's no need to wait for the result.
Hi there,

We cannot officially provide support for async writing as the Unity API itself is not thread safe. However, there are some users using Easy Save in an entirely separate thread with no problems whatsoever. The cost of the actual writing of data to disk is actually negligible compared to the cost of random-access and serialisation logic, so there's little benefit by only doing the writing/reading part of the process in parallel.

If you're not already doing so you might want to look into using ES2Writer/Reader sequentially as the performance benefits are quite large over random-access. You can also use coroutines within the ES2Writer/Reader to spread the Read/Write calls over a number of frames if necessary without having to worry about concurrency issues.

All the best,
Joel
Joel @ Moodkie Interactive
Purchase Easy Save | Contact | Guides | Docs | Getting started
viveleroi
Posts: 17
Joined: Fri Feb 03, 2017 1:40 am

Re: Request a Feature (Post your Requests Here)

Post by viveleroi »

I am performing writes/reads sequentially. I'm not using tags anywhere in my code.

Just by playing I definitely notice a solid lag spike when new chunks generate/old chunks unload and that wasn't there before, so I need to see what else I can do. Profiling shows that ES2.Save (mainly ES2Writer.Write and ES2Writer.Save) now account for almost 50% of my chunk load/unload process.

I'll focus on the actual save calls for the tiles themselves. Each chunk has a dictionary of <Vector2, Tile>, so maybe it would be more efficient to write only the tiles? I'd have to ensure I always write tiles in a derministic order though.

I'm already filtering chunks that haven't been changed, but when there's a new world and every chunk is generating for the first time, that unchanged flag is worthless. That's why I was hoping I could do this in another thread - generating the chunks is pretty efficient and doesn't cause any lag, but except when someone quits the game, saving is more of a set-it-and-forget-it action.
User avatar
Joel
Moodkie Staff
Posts: 4852
Joined: Wed Nov 07, 2012 10:32 pm

Re: Async Support

Post by Joel »

Unfortunately due to the lack of type safety in Unity's API, we cannot support it. However, as previously mentioned people have used it asynchronously without any issue and any issues that arise tend to be due to serialising classes while modifying them in their own code.

Do you have any indication of how much data is stored in each of your chunks? And any indication on how much data is stored to disk for each chunk?

If you show me the code you're using, I might be able to suggest some performance enhancements.

- Joel
Joel @ Moodkie Interactive
Purchase Easy Save | Contact | Guides | Docs | Getting started
viveleroi
Posts: 17
Joined: Fri Feb 03, 2017 1:40 am

Re: Async Support

Post by viveleroi »

I'm always open to suggestions, so I'll show you as much as you need, for now I'll post the relevant type classes and describe how I call saves.

Sidenote first, thanks for the help. You've seen me all over these forums lately as I figure things out. I've been a developer for a long time but I've never worked with C# or unity before, so I'm gradually adjusting to the differences.

Overall, there isn't too much data per chunk/tile. I've mentioned in the compression thread that currently, my chunk files wind up being about 200k. I'd like to inspect them and really get an understanding of what's being written but it's not a priority, since I'm handling what's written in custom type classes I feel like that's exactly what's being written.

The game involves a 2D infinite tile map, so chunks of 3x3 tiles are generated as the player moves. As each chunk is generated, I save it to disk. When chunks are "unloaded" I save them only if the player has made changes (think Minecraft, if the player mined stuff, etc).

I've tested and profile the chunk generation code itself a lot, and it was running very smoothly before I added the save code, but there's clear lag when new chunks are generated. It's vastly more noticeable on large displays, because a wider radius of chunks are touched.

When I generate a new chunk, I immediately call ES2.Save(chunk);.

Every chunk is saved to a separate file - that felt like a better route than trying to make one massive file with tagged entries (plus I've used this technique in other objective-c/java games without issue). For example the very first chunk will be saved in Saves/{worldName}/terrain/0_0.bin.

Chunks are really just a world subdivision which holds 9 tiles, so that's really all this custom type write/read does:

Code: Select all

public class ES2UserType_Chunk : ES2Type
{
	public override void Write(object obj, ES2Writer writer)
	{
		Chunk data = (Chunk)obj;
		writer.Write(data.tiles);
	}
	
	public override object Read(ES2Reader reader)
	{
		Chunk data = new Chunk();
		Read(reader, data);
		return data;
	}

	public override void Read(ES2Reader reader, object c)
	{
		Chunk data = (Chunk)c;
		data.tiles = reader.ReadDictionary<UnityEngine.Vector2,Tile>();
	}
	
	/* ! Don't modify anything below this line ! */
	public ES2UserType_Chunk():base(typeof(Chunk)){}
}
The tile is the bulky part. It saves a Vector representing it's world coordinate, an enum value that identifies the specific type of tile it is, and any "entity" data, which I'll explain below.

Code: Select all

public class ES2UserType_Tile : ES2Type
{
	public override void Write(object obj, ES2Writer writer)
	{
		Tile data = (Tile)obj;
		writer.Write(data.worldVec);
		writer.Write(data.tileType);

		// Write entity data
		writer.Write(data.entity != null);
		if (data.entity != null) {
			writer.Write(data.entity.GetComponent<Entity>().GetId());
		}
	}
	
	public override object Read(ES2Reader reader)
	{
		Tile data = new Tile();
		Read(reader, data);
		return data;
	}

	public override void Read(ES2Reader reader, object c)
	{
		Tile data = (Tile)c;
		data.worldVec = reader.Read<UnityEngine.Vector2>();
		data.tileType = reader.Read<TileType>();

		// Read entity data
		var hasEntity = reader.Read<bool>();
		if (hasEntity) {
			var entityId = reader.Read<string>();
			data.entity = GameObject.Find("EntityList").GetComponent<EntityList>().entityPrefabById(entityId);
		}
	}
	
	/* ! Don't modify anything below this line ! */
	public ES2UserType_Tile():base(typeof(Tile)){}
}
An "entity" is essentially a world object static to this tile. Rocks, trees, grass, etc. All the tile stores is a string ID which lets me find the prefab for that entity.

I haven't looked into the performance of reads, but I definitely notice lag, both during gameplay and in the profiler.

Any suggestions you have would be appreciated, even if they don't apply to performance. If you see anything else I should change let me know.
User avatar
Joel
Moodkie Staff
Posts: 4852
Joined: Wed Nov 07, 2012 10:32 pm

Re: Async Support

Post by Joel »

Hi there,

I have a few suggestions you might be able to use to improve performance:

Firstly, because you're using ES2.Save/ES2.Load rather than ES2Writer/ES2Reader to save your chunks, you're not actually removing the overhead of random access because it still performs the overwrite check. Instead, you should do:
using(var writer = ES2Writer.Create("myFile.txt"))
{
    writer.Write(chunk);
    writer.Save(false); // Stores the data without an overwrite check.
}

using(var reader = ES2Reader.Create("myFile.txt"))
{
    chunk = reader.Read<Chunk>();
}
This code is essentially the fastest you can write data, because it's writing sequentially to a BinaryWriter.

Secondly, in your ES2Type you're also calling GetComponent<Entity> for every tile that you write. If you could cache this Entity then it may improve performance as it's not a particularly fast operation.

Regarding loading, you're also calling GameObject.Find on every chunk that you load in your ES2Type, which is not to be advised as it's a very slow operation. You will be better off caching the EntityList variable somewhere so that you don't have to perform a Find and a GetComponent for every tile.

If you do manage to profile it further and work out what methods in particular are slowing your performance, I'll be happy to look into it further.

All the best,
Joel
Joel @ Moodkie Interactive
Purchase Easy Save | Contact | Guides | Docs | Getting started
viveleroi
Posts: 17
Joined: Fri Feb 03, 2017 1:40 am

Re: Async Support

Post by viveleroi »

Thanks. I've made all of these changes and it seems a lot better, though I have yet to test on a large screen where the lag was way more noticeable.
viveleroi
Posts: 17
Joined: Fri Feb 03, 2017 1:40 am

Re: Async Support

Post by viveleroi »

For simplicity I've stripped everything but save code for the chunks/tiles. I'm hoping you have some additional advice because it's still killing the performance of my terrain generation logic.

Running profiling, my chunk generation logic by itself accounts for an average of 10% of a frame's time. On two systems I've tested, a 27" 2011 iMac and a year-old gaming PC connected to a 60" TV there's no noticeable lag as the player moves around.

With the save logic added in, the chunk generation logic jumps to 30% of a frame and there's clear lag for a second while the save completes. The save call accounts for 22%, which is more than twice the original chunk gen call.

I actually noticed that read calls were much faster but haven't done any profiling yet. For now, I'm deleting the saves entirely and profiling only the fresh writes.

After generating a new chunk, (this logic hasn't changed at all during the save work), the chunk is immediate saved to disk using your recommended method:

Code: Select all

public void SaveChunk(Chunk chunk) {
	// Save the chunk
	using(var writer = ES2Writer.Create(getChunkSavePath(chunk.chunkVec))) {
		writer.Write(chunk);
		writer.Save(false); // Stores the data without an overwrite check.
	}
}
The "write" portion of the chunk type is:

Code: Select all

public class ES2UserType_Chunk : ES2Type {
	public override void Write(object obj, ES2Writer writer) {
		Chunk data = (Chunk)obj;
		writer.Write(data.tiles); // a Dictionary<Vector2, Tile>
	}
	
	// ...
}
... and the "Write" portion of the Tile type is:

Code: Select all

public class ES2UserType_Tile : ES2Type {
	public override void Write(object obj, ES2Writer writer) {
		Tile data = (Tile)obj;
		writer.Write(data.worldVec); // a Vector2
		writer.Write(data.tileType); // an Enum
	}
	
	// ...
}
When running in the Unity editor, my game loads 14 chunks per row as the user moves directly north. That equates to 14 calls to the above SaveChunk method (and Chunk type class). Each chunk holds 9 Tiles. Because each chunk has its own file this produces 14 separate files.

I'm open to any additional advice. Here's a quick screenshot of the profiling: http://i.imgur.com/UyaPi81.png

EDIT: I've converted the Dictionary chunks stored Tiles in to a List - there really was no need for a dict and this produces smaller files. Unfortunately it hasn't had any noticeable impact on the write performance.

I commented out the Writer calls in the Chunk type, so there's no data being written (outside of anything you're writing) and the performance is only a few percentage points lower (16% vs 20%). Given that, it seems clear a lot of the performance cost is from overhead in ES2 and isn't related to my data. Profile from this experiment: https://puu.sh/tWeWO/fd9cb58848.png

I ran a quick test writing the chunk vector only using a BinaryWriter directly. That gave me a bit of a performance boost (average about 6 percentage points lower than the save-nothing test I did with ES2). I think that because my needs for this portion are so specific, it might be better to write custom IO logic for it. Another benefit is that with such level of control I can run this logic async in situations where I don't care about it being synchronous - that does wonders for the perf issue because it doesn't block.
User avatar
Joel
Moodkie Staff
Posts: 4852
Joined: Wed Nov 07, 2012 10:32 pm

Re: Async Support

Post by Joel »

Hi there,

Judging by the profiler screenshot you sent me, most of the overhead is in creating the File rather than Easy Save's overhead (FileStream.ctor accounts for 80% of the save call). This usually suggests that your chunk size is too small. But if you have no control over this, you will certainly be better off writing a custom solution so you can write the data async and ensure your own thread safety.

All the best,
Joel
Joel @ Moodkie Interactive
Purchase Easy Save | Contact | Guides | Docs | Getting started
viveleroi
Posts: 17
Joined: Fri Feb 03, 2017 1:40 am

Re: Async Support

Post by viveleroi »

I chose to put chunks into their own files because: a) as the user walks around I need an easy way to read chunks based on their coordinates, and b) chunk data varies in length depending on the tiles/entities on them. If there's a way to easily do both while combing files somehow, I'd be open to it.

Let me know if you have any suggestions, no matter which route I take I'll be creating the files so that specific issue isn't going away.
Locked