-
Notifications
You must be signed in to change notification settings - Fork 0
Description
This issue shall discuss the borrowing model of the nodes in vnodes
. For those of you who don't know, these nodes will be structured in a tree like this:
├── dev
│ ├── keyboards
│ │ ├── 0
│ │ └── 1
│ └── platform
├── ecs
│ ├── get
│ ├── insert
│ ├── join
│ └── world
│ ├── BarResource
│ └── FooResource
├── io
│ ├── assets
│ │ ├── dwarven_crossbow.gltf
│ │ ├── mesh.obj
│ │ └── terrain.png
│ └── configs
│ └── display.ron
│ ├── msaa
│ ├── resolution
│ │ ├── height
│ │ └── width
│ └── vsync
└── scripts
├── dragons.lua
├── inventory.rhai
└── weapons.rhai
Each identifier above is called a node. Note that just because it is represented as a node, it doesn't mean it has that's that internal representation. Especially for values inside configs, that would be too expensive.
Requirements of the borrowing model
Obviously it should work in parallel. What parts exactly shall work in parallel, where we make exceptions, that's part of this discussion. To find a good model, we need to know which operations are very common and which are not (and thus may be more expensive).
Common operations
- very common: calling a node; this can be a script or an engine function that's exposed via nodes (e.g.
/ecs/insert
to insert a component) - reading a node's internal data, e.g. values from a config file, a resource
- writing to the node when it gets called
Average
- random writes to nodes, e.g. on an event callback
Rare operations
- creating new nodes
- overriding an existing node
- removing an existing node
Selected models with their pros and cons
Static borrowing model
In the static borrowing model, the compiler controls read and write access. Nodes would be retrieved as references from the tree.
Advanges
- very fast: checked at compile-time
- low implementation effort
Disadvantages
- only allows to borrow one node mutably at once
- forces us to either make everything immutable or drop parallelism
Wrap everything with a Mutex
A Mutex
can be locked to get a mutable reference. This would be done internally, and every node would be an Arc<Mutex<Node>>
. Note that deadlocks aren't possible except the node triggers a callback that tries to borrow the same node (but this is an issue with every model I know of).
Advantages
- easy for implementors because they always have mutable access
- if the same node is barely used in parallel, this allows things to run arbitrarily in parallel
Disadvantages
- high overhead
Make every method receive &self
, let user choose how to wrap
If every method of a node (calling, setting a sub node, reading a value, ..) takes an immutable reference, there is no borrowing issue anymore. However, quite some nodes actually do need write access. For that, they would need to wrap their internal data or specific fields with a Mutex
, RwLock
, etc.
Advantages
- improves on the above approach of wrapping everything by default, immutable nodes won't need to do any locking
- atomicity and individual strategies can be used to optimize parallel access
Disadvantages
- additional burden on implementors
- overhead differs, largely dependent on how much the nodes are optimized
Some random ideas
- acquiring tickets upfront, similar to system dependencies in Specs (seems difficult, esp wrt callbacks; might be solved by deferring them)
- creating copies of the data with the risk of reading dirty (outdated) revisions
- working with change sets, possibly STMs
- create a topological sort of the nodes, allowing to lock specific regions (like say
/dev/keyboards
recursively) by an index range
That's all I can think of for now. Please add your ideas and opinions below ;)