Add business pricing and closure controls with compatible digest optimization
This commit is contained in:
@@ -11,6 +11,19 @@ extends RefCounted
|
||||
const FORMAT_VERSION := 1
|
||||
const MAX_NESTING_DEPTH := 128
|
||||
|
||||
# A single digest owns these caches; nothing survives a call or retains snapshot
|
||||
# values. Only private arrays of immutable, homogeneous keys become cache keys.
|
||||
# Separate buckets preserve the distinct String / StringName canonical tags.
|
||||
const MAX_CACHED_LAYOUTS := 256
|
||||
const MAX_CACHED_LAYOUT_KEYS := 32
|
||||
const MAX_CACHED_KEY_LENGTH := 128
|
||||
|
||||
class KeyLayouts:
|
||||
extends RefCounted
|
||||
var strings: Dictionary = {}
|
||||
var names: Dictionary = {}
|
||||
|
||||
|
||||
|
||||
static func compute(value: Variant) -> String:
|
||||
var canonical := canonical_bytes(value)
|
||||
@@ -27,12 +40,12 @@ static func compute(value: Variant) -> String:
|
||||
static func canonical_bytes(value: Variant) -> PackedByteArray:
|
||||
var output := PackedByteArray()
|
||||
_append_ascii(output, "the-tree:simulation-digest:v%d;" % FORMAT_VERSION)
|
||||
if not _encode_value(value, output, 0):
|
||||
if not _encode_value(value, output, 0, KeyLayouts.new()):
|
||||
return PackedByteArray()
|
||||
return output
|
||||
|
||||
|
||||
static func _encode_value(value: Variant, output: PackedByteArray, depth: int) -> bool:
|
||||
static func _encode_value(value: Variant, output: PackedByteArray, depth: int, layouts: KeyLayouts) -> bool:
|
||||
if depth > MAX_NESTING_DEPTH:
|
||||
push_error("SimulationDigest data exceeds the maximum nesting depth")
|
||||
return false
|
||||
@@ -107,9 +120,9 @@ static func _encode_value(value: Variant, output: PackedByteArray, depth: int) -
|
||||
and _append_float64(output, color.a)
|
||||
)
|
||||
TYPE_ARRAY:
|
||||
return _encode_array(value, output, depth)
|
||||
return _encode_array(value, output, depth, layouts)
|
||||
TYPE_DICTIONARY:
|
||||
return _encode_dictionary(value, output, depth)
|
||||
return _encode_dictionary(value, output, depth, layouts)
|
||||
TYPE_PACKED_BYTE_ARRAY:
|
||||
_append_sized_bytes(output, "pb", value)
|
||||
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY:
|
||||
@@ -130,34 +143,73 @@ static func _encode_value(value: Variant, output: PackedByteArray, depth: int) -
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_array(value: Array, output: PackedByteArray, depth: int) -> bool:
|
||||
static func _encode_array(value: Array, output: PackedByteArray, depth: int, layouts: KeyLayouts) -> bool:
|
||||
_append_ascii(output, "a%d:" % value.size())
|
||||
# Event IDs and several saved histories are typed string arrays. Their element
|
||||
# type is guaranteed by Godot, so avoid the recursive Variant dispatch for each
|
||||
# entry while keeping the same depth limit and exact bytes.
|
||||
if value.is_typed() and value.get_typed_builtin() in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||
if not value.is_empty() and depth >= MAX_NESTING_DEPTH:
|
||||
push_error("SimulationDigest data exceeds the maximum nesting depth")
|
||||
return false
|
||||
var tag := "s" if value.get_typed_builtin() == TYPE_STRING else "q"
|
||||
for item in value:
|
||||
_append_sized_bytes(output, tag, String(item).to_utf8_buffer())
|
||||
return true
|
||||
for item in value:
|
||||
if not _encode_value(item, output, depth + 1):
|
||||
if not _encode_value(item, output, depth + 1, layouts):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _encode_dictionary(value: Dictionary, output: PackedByteArray, depth: int) -> bool:
|
||||
var entries: Array[Dictionary] = []
|
||||
for key in value:
|
||||
var encoded_key := PackedByteArray()
|
||||
if not _encode_value(key, encoded_key, depth + 1):
|
||||
return false
|
||||
entries.append({
|
||||
"sort_key": encoded_key.hex_encode(),
|
||||
"encoded_key": encoded_key,
|
||||
"value": value[key],
|
||||
})
|
||||
entries.sort_custom(func(left: Dictionary, right: Dictionary) -> bool:
|
||||
return String(left["sort_key"]) < String(right["sort_key"])
|
||||
)
|
||||
static func _encode_dictionary(value: Dictionary, output: PackedByteArray, depth: int, layouts: KeyLayouts) -> bool:
|
||||
if not value.is_empty() and depth >= MAX_NESTING_DEPTH:
|
||||
push_error("SimulationDigest data exceeds the maximum nesting depth")
|
||||
return false
|
||||
var keys := value.keys()
|
||||
var cached_layouts := _layout_bucket(keys, layouts)
|
||||
var entries: Array = []
|
||||
if not cached_layouts.is_empty() and cached_layouts.has(keys):
|
||||
entries = cached_layouts[keys]
|
||||
else:
|
||||
for key in keys:
|
||||
var encoded_key := PackedByteArray()
|
||||
if not _encode_value(key, encoded_key, depth + 1, layouts):
|
||||
return false
|
||||
entries.append([key, encoded_key, encoded_key.hex_encode()])
|
||||
entries.sort_custom(func(left: Array, right: Array) -> bool:
|
||||
return String(left[2]) < String(right[2])
|
||||
)
|
||||
if _can_cache_keys(keys) and cached_layouts.size() < MAX_CACHED_LAYOUTS:
|
||||
# keys is a fresh private array from Dictionary.keys(). It and entries
|
||||
# are never mutated once stored. Native Dictionary lookup compares full
|
||||
# array contents after hashing, so hash collisions cannot alias layouts.
|
||||
cached_layouts[keys] = entries
|
||||
|
||||
_append_ascii(output, "d%d:" % entries.size())
|
||||
for entry in entries:
|
||||
var encoded_key: PackedByteArray = entry["encoded_key"]
|
||||
output.append_array(encoded_key)
|
||||
if not _encode_value(entry["value"], output, depth + 1):
|
||||
for entry: Array in entries:
|
||||
output.append_array(entry[1])
|
||||
# Read each value live. A reused key layout never caches child containers
|
||||
# or their bytes, including when several dictionaries share the schema.
|
||||
if not _encode_value(value[entry[0]], output, depth + 1, layouts):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _layout_bucket(keys: Array, layouts: KeyLayouts) -> Dictionary:
|
||||
if _can_cache_keys(keys):
|
||||
return layouts.strings if typeof(keys[0]) == TYPE_STRING else layouts.names
|
||||
return {}
|
||||
|
||||
|
||||
static func _can_cache_keys(keys: Array) -> bool:
|
||||
if keys.is_empty() or keys.size() > MAX_CACHED_LAYOUT_KEYS:
|
||||
return false
|
||||
var key_type := typeof(keys[0])
|
||||
if key_type != TYPE_STRING and key_type != TYPE_STRING_NAME:
|
||||
return false
|
||||
for key in keys:
|
||||
if typeof(key) != key_type or String(key).length() > MAX_CACHED_KEY_LENGTH:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user