Add business pricing and closure controls with compatible digest optimization

This commit is contained in:
Takeshi
2026-09-09 12:04:12 +09:00
parent 071e338e82
commit b3b82ef03e
35 changed files with 2203 additions and 59 deletions
@@ -0,0 +1,240 @@
extends RefCounted
## Frozen v1 encoder from commit 627b200, kept only as the byte-compatibility
## oracle and unoptimized benchmark baseline. Do not update with production changes.
##
## Dictionary insertion order never affects the result. Type tags and byte
## lengths make the canonical form unambiguous, and Array order remains
## significant. Runtime Objects and other process-local values are rejected;
## callers must first export them to Dictionaries, Arrays and value types.
const FORMAT_VERSION := 1
const MAX_NESTING_DEPTH := 128
static func compute(value: Variant) -> String:
var canonical := canonical_bytes(value)
if canonical.is_empty():
return ""
var context := HashingContext.new()
if context.start(HashingContext.HASH_SHA256) != OK:
push_error("Could not initialize the simulation digest hash")
return ""
context.update(canonical)
return context.finish().hex_encode()
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):
return PackedByteArray()
return output
static func _encode_value(value: Variant, output: PackedByteArray, depth: int) -> bool:
if depth > MAX_NESTING_DEPTH:
push_error("SimulationDigest data exceeds the maximum nesting depth")
return false
match typeof(value):
TYPE_NIL:
_append_ascii(output, "n")
TYPE_BOOL:
_append_ascii(output, "b1" if bool(value) else "b0")
TYPE_INT:
_append_ascii(output, "i%d;" % int(value))
TYPE_FLOAT:
_append_ascii(output, "f")
return _append_float64(output, float(value))
TYPE_STRING:
_append_sized_bytes(output, "s", String(value).to_utf8_buffer())
TYPE_STRING_NAME:
_append_sized_bytes(output, "q", String(value).to_utf8_buffer())
TYPE_VECTOR2:
var vector: Vector2 = value
_append_ascii(output, "v2")
return _append_float64(output, vector.x) and _append_float64(output, vector.y)
TYPE_VECTOR2I:
var vector: Vector2i = value
_append_ascii(output, "w2")
_append_ascii(output, "i%d;i%d;" % [vector.x, vector.y])
TYPE_RECT2:
var rect: Rect2 = value
_append_ascii(output, "r2")
return (
_append_float64(output, rect.position.x)
and _append_float64(output, rect.position.y)
and _append_float64(output, rect.size.x)
and _append_float64(output, rect.size.y)
)
TYPE_RECT2I:
var rect: Rect2i = value
_append_ascii(output, "x2")
_append_ascii(output, "i%d;i%d;i%d;i%d;" % [rect.position.x, rect.position.y, rect.size.x, rect.size.y])
TYPE_VECTOR3:
var vector: Vector3 = value
_append_ascii(output, "v3")
return (
_append_float64(output, vector.x)
and _append_float64(output, vector.y)
and _append_float64(output, vector.z)
)
TYPE_VECTOR3I:
var vector: Vector3i = value
_append_ascii(output, "w3")
_append_ascii(output, "i%d;i%d;i%d;" % [vector.x, vector.y, vector.z])
TYPE_VECTOR4:
var vector: Vector4 = value
_append_ascii(output, "v4")
return (
_append_float64(output, vector.x)
and _append_float64(output, vector.y)
and _append_float64(output, vector.z)
and _append_float64(output, vector.w)
)
TYPE_VECTOR4I:
var vector: Vector4i = value
_append_ascii(output, "w4")
_append_ascii(output, "i%d;i%d;i%d;i%d;" % [vector.x, vector.y, vector.z, vector.w])
TYPE_COLOR:
var color: Color = value
_append_ascii(output, "c")
return (
_append_float64(output, color.r)
and _append_float64(output, color.g)
and _append_float64(output, color.b)
and _append_float64(output, color.a)
)
TYPE_ARRAY:
return _encode_array(value, output, depth)
TYPE_DICTIONARY:
return _encode_dictionary(value, output, depth)
TYPE_PACKED_BYTE_ARRAY:
_append_sized_bytes(output, "pb", value)
TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY:
return _encode_packed_int_array(value, output)
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY:
return _encode_packed_float_array(value, output)
TYPE_PACKED_STRING_ARRAY:
return _encode_packed_string_array(value, output)
TYPE_PACKED_VECTOR2_ARRAY:
return _encode_packed_vector2_array(value, output)
TYPE_PACKED_VECTOR3_ARRAY:
return _encode_packed_vector3_array(value, output)
TYPE_PACKED_COLOR_ARRAY:
return _encode_packed_color_array(value, output)
_:
push_error("SimulationDigest does not support Variant type %s" % type_string(typeof(value)))
return false
return true
static func _encode_array(value: Array, output: PackedByteArray, depth: int) -> bool:
_append_ascii(output, "a%d:" % value.size())
for item in value:
if not _encode_value(item, output, depth + 1):
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"])
)
_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):
return false
return true
static func _encode_packed_int_array(value: Variant, output: PackedByteArray) -> bool:
var tag := "pi32" if typeof(value) == TYPE_PACKED_INT32_ARRAY else "pi64"
_append_ascii(output, "%s%d:" % [tag, value.size()])
for item in value:
_append_ascii(output, "i%d;" % int(item))
return true
static func _encode_packed_float_array(value: Variant, output: PackedByteArray) -> bool:
var tag := "pf32" if typeof(value) == TYPE_PACKED_FLOAT32_ARRAY else "pf64"
_append_ascii(output, "%s%d:" % [tag, value.size()])
for item in value:
if not _append_float64(output, float(item)):
return false
return true
static func _encode_packed_string_array(value: PackedStringArray, output: PackedByteArray) -> bool:
_append_ascii(output, "ps%d:" % value.size())
for item in value:
_append_sized_bytes(output, "s", item.to_utf8_buffer())
return true
static func _encode_packed_vector2_array(value: PackedVector2Array, output: PackedByteArray) -> bool:
_append_ascii(output, "pv2%d:" % value.size())
for item in value:
if not _append_float64(output, item.x) or not _append_float64(output, item.y):
return false
return true
static func _encode_packed_vector3_array(value: PackedVector3Array, output: PackedByteArray) -> bool:
_append_ascii(output, "pv3%d:" % value.size())
for item in value:
if (
not _append_float64(output, item.x)
or not _append_float64(output, item.y)
or not _append_float64(output, item.z)
):
return false
return true
static func _encode_packed_color_array(value: PackedColorArray, output: PackedByteArray) -> bool:
_append_ascii(output, "pc%d:" % value.size())
for item in value:
if (
not _append_float64(output, item.r)
or not _append_float64(output, item.g)
or not _append_float64(output, item.b)
or not _append_float64(output, item.a)
):
return false
return true
static func _append_float64(output: PackedByteArray, value: float) -> bool:
if is_nan(value) or is_inf(value):
push_error("SimulationDigest rejects NaN and infinite float values")
return false
var normalized := 0.0 if value == 0.0 else value
var encoded := PackedByteArray()
encoded.resize(8)
encoded.encode_double(0, normalized)
output.append_array(encoded)
return true
static func _append_sized_bytes(output: PackedByteArray, tag: String, value: PackedByteArray) -> void:
_append_ascii(output, "%s%d:" % [tag, value.size()])
output.append_array(value)
static func _append_ascii(output: PackedByteArray, value: String) -> void:
output.append_array(value.to_utf8_buffer())
@@ -0,0 +1 @@
uid://cyhefo00b7wkr
@@ -0,0 +1,222 @@
extends SceneTree
const Digest = preload("res://scripts/core/simulation_digest.gd")
const Reference = preload("res://tests/performance/fixtures/simulation_digest_v1_reference.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("Simulation digest: v1 byte compatibility and growing-history benchmark")
_test_value_types()
_test_dictionary_layouts()
_test_mutable_values()
_test_limits_and_rejections()
_test_growing_histories()
print("Digest performance: %d checks, %d failures" % [_checks, _failures])
quit(_failures)
func _test_value_types() -> void:
var values: Array = [
null, false, true, -9223372036854775807, 9223372036854775807,
0, 0.0, -0.0, -0.125, 1.25e100, "", "厨房\u0000🌳", &"", &"運搬",
Vector2(-1.5, 2.25), Vector2i(-3, 7), Rect2(-1.5, 2.25, 3.5, 4.75),
Rect2i(-3, 7, 2, 8), Vector3(-1.5, 2.25, 3.5), Vector3i(-3, 7, 2),
Vector4(-1.5, 2.25, 3.5, 4.75), Vector4i(-3, 7, 2, 8), Color(0.25, 0.5, 0.75, 1),
[], {}, PackedByteArray([0, 127, 255]), PackedInt32Array([-3, 0, 7]),
PackedInt64Array([-9223372036854775807, 9223372036854775807]),
PackedFloat32Array([-1.25, 0.0, 2.5]), PackedFloat64Array([-1.25, -0.0, 1.25e100]),
PackedStringArray(["", "日本語", "🌳"]), PackedVector2Array([Vector2(-1, 2)]),
PackedVector3Array([Vector3(-1, 2, 3)]), PackedColorArray([Color(0.25, 0.5, 0.75, 1)]),
]
var strings: Array[String] = ["", "a", "厨房"]
var names: Array[StringName] = [&"", &"a", &"厨房"]
values.append(strings)
values.append(names)
for index in values.size():
_expect_bytes(values[index], "canonical bytes for value type %d, sample %d" % [typeof(values[index]), index])
_expect_bytes({"values": values}, "nested complete supported value matrix")
_expect(Digest.compute([1, 2, 3]) == "4e32bb2921e1b879d5b8778789d3bc7e46559d4448e95f9c2fed427d7e5c7b93", "published v1 reference digest remains unchanged")
func _test_dictionary_layouts() -> void:
var rows: Array = []
# Native Variant equality can equate String and StringName; cache buckets must
# keep their type tags apart even when the textual schemas are identical.
for index in 60:
var row := {}
var keys: Array = ["z", "aa", "日本語", "a", "\u0000", "", "001"]
if index % 2 == 1:
keys.reverse()
for key: String in keys:
row[StringName(key) if index % 3 == 0 else key] = {"index": index, "key": key}
rows.append(row)
_expect_bytes(rows, "cached String and StringName schemas preserve tags and byte order")
var mixed := {}
mixed["text"] = "String"
mixed[&"name"] = "StringName"
mixed[7] = "integer"
mixed[1.25] = "float"
mixed[Vector2i(2, 3)] = "vector"
mixed[["a", 1]] = "array key"
mixed[{"inner": "key"}] = "dictionary key"
_expect_bytes([mixed, mixed.duplicate(true)], "mixed and mutable dictionary keys use the uncached encoder")
var forward := {"a": 1, "b": 2, "ccc": 3}
var reverse := {"ccc": 3, "b": 2, "a": 1}
_expect(Digest.canonical_bytes(forward) == Digest.canonical_bytes(reverse), "canonical order ignores dictionary insertion order")
# Exceed each bound to check graceful uncached fallback, without relying on a
# process-global cache or on the hash of an incomplete key signature.
var many_schemas: Array = []
for index in 600:
many_schemas.append({"schema_%d" % index: index, "nested": {"value": index}})
_expect_bytes(many_schemas, "more than the bounded layout capacity remains byte-identical")
# These two complete key arrays collide under Godot 4.7's native hash. The
# cache must still compare complete contents, never index by hash alone.
var collision_left := {"schema.96ac180f": 1, "value": 2}
var collision_right := {"schema.c5015a12": 3, "value": 4}
if hash(collision_left.keys()) == hash(collision_right.keys()):
print(" Native layout hash-collision fixture exercised")
_expect_bytes([collision_left, collision_right, collision_left], "different key layouts with colliding native hashes remain distinct")
var many_keys := {}
for index in 80:
many_keys["key_%d" % index] = index
_expect_bytes(many_keys, "large dictionaries remain byte-identical")
_expect_bytes({"長".repeat(200): 1}, "long keys bypass the cache")
func _test_mutable_values() -> void:
var nested := {"history": [{"minute": 1, "amount": 3}], "queue": [1, 2]}
var before := Digest.compute(nested)
nested["history"][0]["amount"] = 4
nested["queue"].append(3)
_expect_bytes(nested, "nested value mutations cannot reuse stale encoded values")
_expect(Digest.compute(nested) != before, "nested mutation changes the hash")
nested.erase("queue")
nested[&"queue"] = [1, 2]
_expect_bytes(nested, "key type mutation between digest calls is observed")
var left := {"key": {"value": 1}}
var right := {"key": {"value": 2}}
_expect_bytes([left, right], "reused schema reads each dictionary's current values")
var canonical := Digest.canonical_bytes(nested)
canonical.fill(0)
_expect_bytes(nested, "caller mutation of previous output cannot corrupt another call")
func _test_limits_and_rejections() -> void:
var typed_leaf: Array[String] = ["leaf"]
_expect_bytes(_nested(typed_leaf, 127), "typed string item at exact nesting limit is valid")
_expect_bytes(_nested({"leaf": 1}, 127), "cached dictionary key at exact nesting limit is valid")
_expect_bytes(_nested([], 128), "empty container at nesting limit is valid")
var rejected: Array = [_nested(typed_leaf, 128), _nested({"leaf": 1}, 128), _nested(null, 129), NAN, INF, -INF, Vector2(NAN, 1), PackedFloat64Array([INF]), {"nested": NAN}, NodePath("runtime/object")]
var object := RefCounted.new()
rejected.append(object)
var errors_enabled := Engine.print_error_messages
Engine.print_error_messages = false
for index in rejected.size():
var legacy: PackedByteArray = Reference.canonical_bytes(rejected[index])
var current: PackedByteArray = Digest.canonical_bytes(rejected[index])
_expect(legacy.is_empty() and current.is_empty(), "unsupported or over-depth sample %d still rejects" % index)
Engine.print_error_messages = errors_enabled
func _test_growing_histories() -> void:
var reports: Array[Dictionary] = []
for history_count in [100, 1000, 5000]:
var snapshot := _history_snapshot(history_count)
_expect_bytes(snapshot, "growing-history snapshot with %d records" % history_count)
_expect(Digest.compute(snapshot) == Reference.compute(snapshot), "growing-history SHA-256 with %d records" % history_count)
# Warm both encoders, then alternate execution order. Timing is reported,
# never asserted: CPU load and editor/preview activity vary across machines.
Reference.compute(snapshot)
Digest.compute(snapshot)
var reference_samples: Array[float] = []
var optimized_samples: Array[float] = []
for sample in 5:
if sample % 2 == 0:
reference_samples.append(_measure(Reference.compute, snapshot))
optimized_samples.append(_measure(Digest.compute, snapshot))
else:
optimized_samples.append(_measure(Digest.compute, snapshot))
reference_samples.append(_measure(Reference.compute, snapshot))
reference_samples.sort()
optimized_samples.sort()
var reference_ms: float = reference_samples[2]
var optimized_ms: float = optimized_samples[2]
var report := {
"history_records": history_count,
"canonical_bytes": Digest.canonical_bytes(snapshot).size(),
"baseline_median_ms": snappedf(reference_ms, 0.001),
"optimized_median_ms": snappedf(optimized_ms, 0.001),
"speedup": snappedf(reference_ms / optimized_ms, 0.001),
}
reports.append(report)
print("DIGEST_BENCHMARK %s" % JSON.stringify(report))
print("DIGEST_PERFORMANCE_REPORT %s" % JSON.stringify({"samples_per_encoder": 5, "metric": "median wall time including SHA-256", "fixture": "synthetic event, ledger, and service histories; complete histories retained", "results": reports}))
var ids: Array[String] = []
for index in 10000:
ids.append("facility.operation.finished:%08d:reception.1:guest.%08d" % [index, index / 4])
var id_snapshot := {"version": 1, "published_ids": ids, "recent_events": []}
_expect_bytes(id_snapshot, "append-only published event ID history remains complete")
var id_reference_samples: Array[float] = []
var id_optimized_samples: Array[float] = []
for sample in 5:
id_reference_samples.append(_measure(Reference.compute, id_snapshot))
id_optimized_samples.append(_measure(Digest.compute, id_snapshot))
id_reference_samples.sort()
id_optimized_samples.sort()
print("DIGEST_EVENT_HISTORY_BENCHMARK %s" % JSON.stringify({"published_ids": ids.size(), "canonical_bytes": Digest.canonical_bytes(id_snapshot).size(), "baseline_median_ms": snappedf(id_reference_samples[2], 0.001), "optimized_median_ms": snappedf(id_optimized_samples[2], 0.001), "speedup": snappedf(id_reference_samples[2] / id_optimized_samples[2], 0.001)}))
func _history_snapshot(count: int) -> Dictionary:
var published_ids: Array[String] = []
var recent_events: Array[Dictionary] = []
var entries: Array[Dictionary] = []
var receipts: Array[Dictionary] = []
for index in count:
var event_id := "event.economy.ledger.%08d" % index
published_ids.append(event_id)
entries.append({
"entry_id": "entry.%08d" % index, "absolute_minute": index * 3,
"entry_type": "operating_expense" if index % 3 == 0 else "service_revenue",
"amount": -12 if index % 3 == 0 else 35, "account_id": "cash",
"counterparty_id": "guest.%05d" % (index % 250), "reference_id": event_id,
"metadata": {"facility_id": "cafeteria.1", "quantity": 1, "description": "基本の食事"},
})
receipts.append({"receipt_id": "receipt.%08d" % index, "minute": index * 3, "guest_id": "guest.%05d" % (index % 250), "facility_id": "cafeteria.1", "satisfied": true, "price": 35})
if index >= maxi(count - 128, 0):
recent_events.append({"event_id": event_id, "event_type": "ledger.posted", "absolute_minute": index * 3, "source_service": "economy", "entity_ids": ["cafeteria.1"], "payload": {"amount": 35, "quantity": 1}, "causation_command_id": index})
return {"version": 7, "minute": count * 3, "domain_events": {"version": 1, "published_ids": published_ids, "recent_events": recent_events}, "economy": {"version": 1, "balance": 5000 + count * 12, "entries": entries}, "operations": {"version": 1, "receipts": receipts}}
func _nested(leaf: Variant, levels: int) -> Variant:
var result: Variant = leaf
for index in levels:
result = [result]
return result
func _measure(encoder: Callable, snapshot: Dictionary) -> float:
var started := Time.get_ticks_usec()
encoder.call(snapshot)
return float(Time.get_ticks_usec() - started) / 1000.0
func _expect_bytes(value: Variant, label: String) -> void:
var expected: PackedByteArray = Reference.canonical_bytes(value)
var actual: PackedByteArray = Digest.canonical_bytes(value)
_expect(not expected.is_empty() and actual == expected, label)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" PASS: %s" % label)
else:
_failures += 1
push_error(" FAIL: %s" % label)
@@ -0,0 +1 @@
uid://c78f41p838v73