From 8d8a1e0634e7d9600436644174ea91ce3c4b8624 Mon Sep 17 00:00:00 2001 From: Nicholas Novak <34256932+NickyBoy89@users.noreply.github.com> Date: Thu, 14 Dec 2023 02:23:51 -0800 Subject: [PATCH] change: Finished almost my last draft on the paper --- paper/document.tex | 229 ++++++++++++++++++------------------ paper/references.bib | 8 ++ paper/unity-file.drawio | 53 +++++++++ paper/unity-file.drawio.png | Bin 0 -> 17333 bytes 4 files changed, 178 insertions(+), 112 deletions(-) create mode 100644 paper/unity-file.drawio create mode 100644 paper/unity-file.drawio.png diff --git a/paper/document.tex b/paper/document.tex index 2e15f5c..8b7597c 100644 --- a/paper/document.tex +++ b/paper/document.tex @@ -27,7 +27,7 @@ or three-dimensional pixels. % Applications of voxels A voxel\cite{enwiki:1186283262} represents a single point or cube in a three-dimensional grid, at a variable size. This feature allows them to -approximately model many three-dimensional structures, in order to reduce the +approximately model many three-dimensional structures, and to reduce the computational complexity in analyzing the shape, which has led to many data-related use cases outside of computer science. For example, to model the inner workings of the brain, Neuroscientists track oxygen concentration through @@ -37,7 +37,7 @@ reflections for visual effects\cite{museth2013vdb}. The output of MRI scans in hospitals are very high-resolution voxel grids. Most recently, machine learning models are being trained on the LIDAR data from self-driving cars\cite{li2020deep} in order to better process their environments. However, -voxels are not often thought of as a way to store three-dimensional shapes, and +voxels are not often thought of as a way to permanently store three-dimensional shapes, and existing research focuses mainly on efficiently representing and processing shapes. My approach models this problem of voxel storage and representation, and turns it into a problem of database design. @@ -205,9 +205,7 @@ advantage of this speedup. In VDB\cite{museth2013vdb} Museth demonstrates that by modeling a sparse voxel grid in different resolutions, a computer cluster can efficiently approximate a physical structures such as a cloud, in order to calculate expensive lighting operations. - -\subsection{Parallel Processing on Voxel Databases} - +% Parallel processing on voxels Williams\cite{williams1992voxel} expands upon the uses of a voxel database to model graph and mesh-based problems. Taking advantage of the parallelism in the grid, many problems can be reframed in the representation of voxels, and solve @@ -216,7 +214,7 @@ voxel is stored in shared memory, making this process only viable to solve problems that can be modeled on one machine, and are far more computationally expensive, rather than data-intensive. -\subsection{Large Voxel Data Set Processing} +\subsection{Storing Large Voxel Data Sets} Another approach to the problem of storing voxel data is the distributed approach in Gorte et. al. \cite{gorte2023analysis}. Since memory is limited @@ -229,6 +227,28 @@ of the data that they are working on. In the paper, Gorte acknowledges the need to split large datasets up into smaller regions, which is similar to the concept of ``chunks'' in my implementation. +\subsection{Chunk Systems in Other Games} + +The decision to choose chunks to represent game data has many justifications. As +\cite{gorte2023analysis} mentions, an infinite grid of voxels needs to be broken +up in a way where applications can store data in an efficient way, and many +other games converge on this same implementation. Another voxel-based game, +Veloren\cite{https://veloren.net} uses the same chunk-based system, although +differs in its storage method. The game switches between several different +storage implementations in each chunk, depending on how dense or sparse the voxel +data within the chunk is. For sparser data, the game stores block information in +a simple key-value hash map. As the number of voxels increase, the game further +breaks this information up, and creates several smaller sections within the +chunk. Finally, for very dense data, the game stores a compressed version using +Zlib compression\cite{veloren32}. This gives many options for data compression +in my database, but also shows how the database can be adapted to store sparser +structures more efficiently if the focus of the project ever needs to change. +Since this game is not based on Minecraft, but an independent project named cube +world, the game comes up with a similar data structure, and shows the +performance considerations for using such a structure. The benchmarks that they +show suggest about an order-of-magnitude improvement over using a key-value +store. + \subsection{Previous Special-Purpose Databases} The design of my database was also inspired by the LSM tree and data-driven @@ -242,11 +262,14 @@ and replicate these in real-time. \section{Methods} -Almost every part of the database was designed so that most operations could be -done in constant time. +\subsection{The Interface for the Database} + +For developers to interact with the database, the database is implemented as a +library, and the database provides a simple application programming interface to +read and write data, consisting of the following operations. The performance +considerations for each of these operations can be found in the methods section +below. -The database provides a simple interface to read and write data, consisting of -the following: \begin{itemize} \item Read a single block \item Write a single block @@ -254,34 +277,44 @@ the following: \item Read a pre-defined ``chunk'' of blocks \end{itemize} +\subsection{Reading and Writing a Single Voxel} -The process of fetching the data for a single point in the world starts at that -point's $x, y$ and $z$ location. The world is infinite in size on the horizontal -$x$ and $z$ axes, but limited in the vertical $y$ axis. In my database, the -world is composed of an infinite grid of ``chunks'', or columns that are a fixed -16 x 16 blocks in the $x$ and $z$ axes, but 256 blocks in the vertical $y$ axis. +The process of updating the data for a single point in the world starts with the +voxel's position. Because the world is infinite on the horizontal $x$ and $z$ +axes, this is implemented by a system of ``chunks'', which are fixed-size 16x16 +columns of voxels, 256 voxels high. The size of these chunks are chosen so that +they are large enough to be efficiently cached, and many operations can occur +within the same chunk, but not too large to the point where the hundred or so +chunks sent to the user upon joining the world cause a network slowdown. Given a +point's $x$ and $z$ positions, the chunk that that voxel belongs to can be found +with a fast modulus operation, in constant time. -Once you know a point's location, you can find with a modulus what chunk the -point is located within. From there, the database only needs to retrieve the -data for the chunk stored at that location. +To fetch the data for that chunk, the database needs to read that data from +disk. The database stores this information in combined files that I call ``unity +files'' (shown in figure \ref{fig:unity}), which consist of a single file on disk, but with the encoded data for +each chunk stored as a start index and size, so that the \verb|seek| syscall can +be used to efficiently query this data, while only keeping one file open. This +scheme was used over the previous system of storing chunk files separately, +because the filesystem had a hard time searching through the hundreds of +thousands of chunks in larger worlds. This start position and size are stored in +an auxillary hash map that stores a mapping of every chunk's position to its +metadata within the unity file. This structure uses a minimal amount of memory, +and also allows for a file to be fetched from disk in a constant amount of time +and disk reads. -Initial implementations for my database focused on tree-based approaches for -finding the files for chunks, but with their complexity and non-constant -complexity, I decided to store each chunk separately. However, with worlds with -chunk counts in the hundreds of thousands, the filesystem implementations had -issues with searching through so many files, which led to performance problems. -Finally, I settled on merging all the chunk data into one file, and use the -filesystem's \verb|seek| syscall to lookup the offset for the correct chunk. A -simple hash table was then used to store each chunk's location with its offset -in the file, which keeps the memory cost low, even with chunk counts in the -millions. This allows for constant-time searches for the chunk's data. +\begin{figure} + \centering + \includegraphics[width=8cm]{unity-file.drawio.png} + \caption{The Layout of a Unity File} + \label{fig:unity} +\end{figure} -Once a chunk is retrieved from disk, the format of the chunk is broken down into -smaller cubic slices of the chunk, called ``sections'' each section is a -16x16x16 cubic area that keeps an index for every chunk. The point's $y$ -position tells the database what section the point is in, and a simple formula -is done to convert the remaining $x$ and $z$ axes into an index within the -section. +Each chunk is further divided into sections, in this case each chunk consists of +16 stacked 16x16x16 cubes of voxels, which results in a total of 4096 block +states per section. Using the voxel's $y$ position, the section for a block can +be found with another modulus. Once this is found, a perfect hash function is +used to map the voxel's position to an array index within the section. Again, +both of these steps are done in constant time respectively. Every section additionally stores a look-up-table, that stores a mapping of a \textit{palette index} to the state of a block. When the value for the point is @@ -289,8 +322,8 @@ retrieved from the section, the value returned is not the block's state, but simply an index into this palette. The palette lookup is done in constant time, and when a new block is added into the section that needs an additional state in the palette, this value is added in constant time as well. The existence of this -palette supports the efficient operation of another part of the database, which -is the ability to change large portions of blocks in the world. +palette supports the efficient operation changing large portions of blocks in +the world. Once the value of the point is found in the palette, the value can be returned to the user. A visual diagram of this process can be found in figure @@ -407,28 +440,29 @@ chunks, so that chunk data could be retrieved without decoding the entire chunk. However, this would require a much more constrained data layout, and limit the implementation of different voxels. -Additionally, compression +Additionally, compression would also reduce the amount of data sent from the +disk to the application. \section{Ethical Considerations} \subsection{Considerations of Computing Resources} -Since databases are at the core part of most complex systems, they are often -built to be run on hardware that the normal consumer can afford +Since a database is at the core part of most software systems, it is important +that the database is designed to work on a wide variety of computers, in order +to ensure all parties are able to take advantage of the improvements. I +designed my database to run on entry-level commodity hardware, as well as +alongside existing application programs that can require far more resources. +Additionally, by focusing on disk storage, which is far cheaper than equivalent +capacities of memory, this further allows researchers or individuals to run +large datasets on a single machine. + +My system targets far less memory usage than existing commercial applications \footnote{\url{https://docs.oracle.com/en/database/oracle/oracle-database/12.2/ntdbi/oracle-database-minimum-hardware-requirements.html}} \footnote{\url{https://wiki.lustre.org/Lustre_Server_Requirements_Guidelines}}. +In the design of my application I had to take advantage of as much of the +computing hardware as possible, but make sure that the approachability and +accessibility for the application does not decrease as as result. -The large hardware requirements of these databases come from the environments -where they are implemented, and at many of these companies, the ability to -keep buying faster hardware allows the company to work on other things that are -more important. However, what this does to the player is effectively prices them -out of the game that they would be already playing, especially since the -database would also have to run alongside the existing Java application of -Minecraft, which quickly exhaust system memory. - -In the design of my server I have to prioritize both performance to take -advantage of the existing hardware, but make sure that the accessibility for -the application does not decrease as a result. \subsection{Considerations of Complexity} Another factor to consider in the implementation of my database is how complex @@ -436,22 +470,20 @@ the existing systems are. Some of the most popular SQL databases, PostgreSQL and MySQL have 1.4 and 4.4 million lines of code respectively \footnote{\url{https://news.ycombinator.com/item?id=24813239}}. -With so much complexity going on, this significantly decreases the overall -knowledge of the system, as well as the individual user who has to debug their -game. Most of this is from the large amount of query logic that handles caching -and speeding up certain queries, so knowing more about the specific problem that -I am trying to solve removes this process from having to be done. - -Especially since most of the people in the Minecraft community are volunteers in -the open-source community, debugging this large of an application would be out of -scope for enjoying a game, and likely lead to it being replaced with something -more simple. The reliability characteristics are also less than what are -required for Minecraft, since they are being compared against a single-threaded -Java program which has been tested to do the correct thing. +Because these systems are so complex, this decreases the number of people who +can effectively work with these systems and maintain them, effectively limiting +this role to larger companies that can afford teams of people to solve these +problems for them. By not focusing on the significant complexity that comes with +caching logic, and keeping a simple implementation for the server, I allow more +companies and developers to use this database for their own needs, and expand +with them. In addition, many decisions were made to help in the debugging +process, including the choice of JSON serialization for the chunk data, which +allows users to read the contents of files easier, and recover potentially +corrupted data. \subsection{Considerations in Security} -Since these databases are very complex, there is also the risk that having a +Since databases are very complex, there is also the risk that having a server exposed over the internet through the Minecraft game server might leave it exposed to attacks. While this is a large issue, an even more important implication is the ability to configure the database correctly. Since these @@ -461,37 +493,31 @@ breaches\footnote{\url{https://www.zdnet.com/article/hacker-ransoms-23k-mongodb- that involve a single server, even at larger companies that have dedicated teams that involve a data breach. -My plan to mitigate this risk is to implement the database in a memory-safe -programming language, which should remove the risk class of memory-unsafety +I mitigate this risk by implementing the database in a memory-safe +programming language, Go, which should remove the risk class of memory-unsafety bugs, which account for around 70\% of all bugs in the Chromium browser engine\footnote{\url{https://www.chromium.org/Home/chromium-security/memory-safety/}}, which is entirely written in non-memory safe C++. -And if the database information is ever able to be leaked through the Minecraft -protocol, the attacker would have access to the full data, because I am planning -to store it unencrypted for performance reasons, and rely on the encryption of -the Minecraft client. And, the data involved does not involve personally -identifying information, so the usefulness of the data would be close to -nothing. - -But, perhaps the most important security risk is if an attacker is able to -access the database directly and bypass all the isolation in the Minecraft -protocol, in order to wipe or corrupt the data for malicious reasons. This would -likely lead to the Minecraft server being unable to be played, and degrade the -experience of the players. It is my plan to take advantage of the limitations of -the types of Minecraft items to provide resilience and easy backups to the -system, because of the purpose-built nature of the system -\footnote{\url{https://twitter.com/eatonphil/status/1568247643788267521?s=20}}. +However, there is the possibility that information stored in the database is +exposed, whether the database not secured, or exposed via an application error. +With this, my database follows the previous threat model of many other +databases, and leaves the security up to the user implementing the application. +Implementing features such as encryption would provide some additional layer of +security, but would also likely decrease performance and increase complexity, +which are also harmful to security in their own ways. Ultimately, I rely on a +setting of defaults that doesn't many any assumptions about the security of the +system. \subsection{Considerations in Fairness} In the implementation of databases, it can often be beneficial to make certain operations faster, at the expense of others that are not done as often. For -instance, if I notice that players often pull items in and out of their systems -often, but almost never search through the list of items, I can take advantage -of this to speed up the database for the most common operations. However, this -can be problematic if the things that I choose to sacrifice affect a certain -group of users. +instance, if I notice that researchers often write more to the database, and +adjust the application accordingly, I can take advantage of this assumption to +speed up the database for the most common operations. However, this can be +problematic if the things that I choose to sacrifice affect a certain group of +users. This tradeoff between speed and reliability occurs so often in Computer Science and is described in terms of percentiles. For instance, if we notice that some @@ -501,15 +527,9 @@ Similarly, if an event only occurs 1\% of the time, we can say it occurs in the like this is make is written about by Google \cite{dean2013tail}, who have to make every decision like this at their scale. -My plan is to not have any tradeoffs that affect the normal gameplay of the -server, and keep it within the 50ms timeframe that the Minecraft has allocated -to itself. Apart from this, one of the main goals of the project is to give -consistent performance, so any further decisions will be made around the -existing implementation of the Minecraft server. - -%https://www.embedded.com/implementing-a-new-real-time-scheduling-policy-for-linux-part-1/ -%https://www.kernel.org/doc/html/latest/scheduler/sched-design-CFS.html -%https://helix979.github.io/jkoo/post/os-scheduler/ +My database plans to keep a consistent set of gaurantees in regards to the +complexity of the basic operations, and provide constant-time operations for +most of these operations. \subsection{Considerations in Accessibility} @@ -518,24 +538,9 @@ require a certain type of computer. Requiring a certain operating system or a more powerful computer would limit access to many of the people that were playing the game before. -However, by basing the goal of the project on improving the performance of the -already existing implementation, any improvements would result in more people -being able to play than before. Also, by designing the system for normal -hardware and in a cross-platform way, this does not limit the people that are -able to access the improvements. - - -\subsection{Considerations in the Concentration of Power} - -With any improvements to performance to servers in Minecraft, this would allow -many of the larger hosting companies, who rent servers monthly to individual -people, to drive down their hosting costs, and allow them to have larger returns -over the smaller providers. However, since this market is so competitive between -companies, because of how easy it is to set up a company, and the options -between companies aren't very different, I would expect any improvement to be -quickly disappear into the competitive market, and benefit everyone equally. - -\section{Future Work, and Conclusion} +However, with the previous performance goals, as well as an implementation in a +portable language, the program is available for as many systems as the Go +compiler supports. \printbibliography diff --git a/paper/references.bib b/paper/references.bib index 7cc93b2..3c06d29 100644 --- a/paper/references.bib +++ b/paper/references.bib @@ -305,3 +305,11 @@ How storage works in database systems, and the evolution of how data is stored year={2010}, publisher={ACM New York, NY, USA} } + +@misc{veloren32, + title = "This Week In Veloren 32", + author = "AngelOnFira", + month = "September", + year = "2019", + url = "https://veloren.net/blog/devblog-32/" +} diff --git a/paper/unity-file.drawio b/paper/unity-file.drawio new file mode 100644 index 0000000..c0c22b0 --- /dev/null +++ b/paper/unity-file.drawio @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/paper/unity-file.drawio.png b/paper/unity-file.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..7748b8e1622dfc662202c14af84a48e660aa0417 GIT binary patch literal 17333 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

esMtzTP%v`&)qdL~ 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