dev-docs/RFCs/v7.2/partial-updates-rfc.md
This RFC proposes a system where if only a single or a few data items have changed, deck.gl can do partial updates of one or more vertex attributes, avoiding iterating over potentially hundreds of thousands of JavaScript elements and updating multiple megabytes of vertex attribute data. The pilot use case is to allow "visual dragging" of a few items in a 100K segment layer to happen at 60PFS.
Today deck.gl can only do a complete update of an entire vertex attribute. We do have the very useful updateTriggers and invalidateAttribute mechanisms that control which attributes get refreshed, but each attribute always gets a complete rebuild when data changes.
Adding support for partial data updates here means that existing vertex arrays and buffer are preserved and memory would be updated/recalculated only for certain objects in the data array.
These are possible assumptions that can be made that simplify the discussion and implementation.
Note that while only updating an attribute for a single object is quite straightforward to implement for instanced attributes where each data object corresponds to exactly one vertex, there is a complication for non-instanced attributes in that the size of objects (i.e. number of vertices per object) can change, and this is likely important to support. There is more details about how this could be handled below.
Note about limitations: We can certainly expand this system to handle multiple points or multiple non-overlapping ranges. The limitations in the assumptions are not fundamental, but intended to simplify discussions around an initial implementation.
Layers take new properties that limit the range of data being updated
dataRange prop - Update a range [start, end] of items from start to end. Activates partial update if supplied.dataStart, dataEnd props - Update a range [start, end] of items from start to end. Activate partial update if supplied.AttributeManager.update supplies additional parameters, start and end.
Layer attribute updaters should be updated to iterate from start to end only and not over the entire range.For instanced layers the change is fairly small. See the current code in ScatterplotLayer:
calculateInstancePositions({value}) {
const {data, getPosition} = this.props;
let i = 0;
for (const point of data) {
const position = getPosition(point);
value[i++] = get(position, 0);
value[i++] = get(position, 1);
value[i++] = get(position, 2) || 0;
}
}
New code:
calculateInstancePositions({value, start, end}) {
const {data, getPosition} = this.props;
for (let i = start; i < end; i++) {
const position = getPosition(data[i]);
value[i++] = get(position, 0);
value[i++] = get(position, 1);
value[i++] = get(position, 2) || 0;
}
}
Note that this function (and the proposed changes to it), could potentially be completely replaced by “auto attribute updating” as the metadata provided to AttributeManager.add already contains essentially all the information needed (it may need a defaultValue as well).
Current code (PathLayer)
calculateInstancePositions(attribute) {
const {data, getPosition} = this.props;
const {value} = attribute;
let i = 0;
for (const point of data) {
const position = getPosition(point);
value[i++] = get(position, 0);
value[i++] = get(position, 1);
value[i++] = get(position, 2) || 0;
}
}
Which could be changed to
calculateStartPositions( {value, start, end}) {
const {paths} = this.state;
for (let i = start; < end; ++i) {
const path = this.state.paths[i]
const numSegments = path.length - 1;
for (let ptIndex = 0; ptIndex < numSegments; ptIndex++) {
const point = path[ptIndex];
value[i++] = point[0];
value[i++] = point[1];
value[i++] = point[2] || 0;
}
});
}
To support non-instanced attributes we could add two new parameters to
AttributeManager.addgetVertexCount - This returns the number of vertices in a specific object. During an incremental update this can be called to discover how many vertices are needed for a particular datum. By calling getVertices - We could potentially support a function that injected the vertices for the object directlyWith additions to automatic attribute updating this could be changed to something like (note that these are functions that are called once per object, not the entire array)
// Called on each object to “size up” array before allocation
// Normally only called when new data is supplied, but is also
// called every time a data range is provided
getVertexCount(index, object) {
return this.state.paths[index].length;
}
// Called on each object to fill in the verts for that object
// for one attribute
// Will be called for all objects or just for some
getStartPositions({index, object, value, offset}) {
const numSegments = object.length - 1;
for (let ptIndex = 0; ptIndex < numSegments; ptIndex++) {
const point = path[ptIndex];
value[offset++] = point[0];
value[offset++] = point[1];
value[offset++] = point[2] || 0;
}
}
… /// etc for other attributes...
The attribute manager already supports “automatic updaters” (these would work for most of our instanced layers). We could mitigate a lot of the need to change layer attribute updater code by moving to automatic instance updaters. It would make our layer code shorter. To date we have been resisting this because of concerns of adding too much magic to the layers, but it may not be practical to resits
Considerations
If the size of non-instanced objects change during partial update, the pickingColors attribute will also be updated to ensure the new subranges refer to the right picking color.
This will somewhat complicate the updaters. This is partly an issue because we pride ourselves on how easy it is to read and understand deck.gl layer source code.
Obviously, if non-instanced attributes are to be supported, we may need to add additional library support for this, as the amount of code needed to manage a variable amount of vertices and indices per object will certainly be too complicated to implement in each attribute updater function.
While instanced attributes have a fixed size per instance (datum), non-instanced attributes can have a variable number of vertices per datum (think paths and polygons). Non-instanced layers are some of the most interesting for editing, containing paths and polygons and can not be excluded from this proposal. Updating a range of non-instanced elements can require a reallocation of all attributes, or just a series of memory copies within the attributes to compact the data A memory compacting/memory expansion/data copying helper library will be helpful. Such a library should have solid benchmark tests to ensure it is truly performant.
A change sub range of memory in a GPU buffer can be updated using gl.bufferSubData
Shrinking WebGL Buffers should not be reallocated. We just need to track bytes allocated and bytes used separately, and we can shrink and grow inside the existing buffer as long we have enough bytes.
Under WebGL2 the copying of data when reallocating buffers to handle expansion can be done on GPU. (WebGL buffers cannot be exapanded once initialized, that requires recreating or reinitializing them)
Partial data updates could also benefit composite layers such as the GeoJson layer. These do not typically use the AttributeManager and can not benefit from any support here. Pass through of start/end indices to the rendered layers can be non-trivial, but not doing so would defeat the value of partial updates for composite layers Solutions could involve a support library for CompositeLayers, or left for a second iteration, stating that in this version, apps should use primitive layers if they need partial updates.
Naturally there are techniques to get around the performance limitations related to monolithic data updates without implementing support for incremental updates
These all require complications on the application side that are desirable to avoid
Since this is expected to be implemented as a backward compatible “improvement” feature, no significant efforts are needed from the existing users.
Considerable effort has been made to improve deck.gl in the areas of