Skip to content

Fix value nodes returning empty literals when read concurrently - #10518

Draft
tobias-tengler wants to merge 4 commits into
mainfrom
tte/fix-value-node-concurrent-reads
Draft

tobias-tengler wants to merge 4 commits into
mainfrom
tte/fix-value-node-concurrent-reads

Conversation

@tobias-tengler

@tobias-tengler tobias-tengler commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Fixes #10506

Value nodes cached their lazily formatted UTF-8 text in a 32-byte ReadOnlyMemorySegment field. That write is not atomic, so two threads reading a node for the first time could see a half-written segment and get an empty literal, which surfaced as SDL like @constraint(minLength: ).

This PR publishes every lazily computed value through a single reference or a volatile flag. IntValueNode formats in its constructor and parses on conversion. FloatValueNode keeps one inline double and a volatile state byte: a node built from text caches the parsed double behind the state, a node built from a double formats its text lazily and publishes it behind the state, and a node built from a decimal formats eagerly. StringValueNode publishes its encoded bytes behind a volatile flag. EnumValueNode keeps its string in one reference field and swaps in a small holder with the string and its UTF-8 bytes once encoded. No node is larger than on main.

Benchmarks

The float and string rows for a new text node and the enum AsSpan() rows were re-measured after def4ec3, which trims the text constructors and lets EnumValueNode.AsSpan() inline, with main and the PR in the same session. Every other row is from the original pass.

IntValueNode

Method Version Mean Ratio Allocated Alloc Ratio
New node from UTF-8 text main 9.80 ns 1.00 64 B 1.00
New node from UTF-8 text PR 9.44 ns 0.96 56 B 0.88
New node from UTF-8 text, ToByte() main 16.97 ns 1.00 104 B 1.00
New node from UTF-8 text, ToByte() PR 11.35 ns 0.67 56 B 0.54
New node from UTF-8 text, ToSByte() main 17.19 ns 1.00 104 B 1.00
New node from UTF-8 text, ToSByte() PR 11.41 ns 0.66 56 B 0.54
New node from UTF-8 text, ToInt16() main 17.36 ns 1.00 104 B 1.00
New node from UTF-8 text, ToInt16() PR 11.77 ns 0.68 56 B 0.54
New node from UTF-8 text, ToUInt16() main 16.88 ns 1.00 104 B 1.00
New node from UTF-8 text, ToUInt16() PR 11.21 ns 0.66 56 B 0.54
New node from UTF-8 text, ToInt32() main 17.33 ns 1.00 104 B 1.00
New node from UTF-8 text, ToInt32() PR 11.76 ns 0.68 56 B 0.54
New node from UTF-8 text, ToUInt32() main 17.24 ns 1.00 104 B 1.00
New node from UTF-8 text, ToUInt32() PR 11.74 ns 0.68 56 B 0.54
New node from UTF-8 text, ToInt64() main 17.65 ns 1.00 104 B 1.00
New node from UTF-8 text, ToInt64() PR 11.62 ns 0.66 56 B 0.54
New node from UTF-8 text, ToUInt64() main 16.81 ns 1.00 104 B 1.00
New node from UTF-8 text, ToUInt64() PR 11.30 ns 0.67 56 B 0.54
New node from UTF-8 text, ToDouble() main 19.96 ns 1.00 64 B 1.00
New node from UTF-8 text, ToDouble() PR 11.49 ns 0.58 56 B 0.88
New node from UTF-8 text, AsSpan() main 11.05 ns 1.00 64 B 1.00
New node from UTF-8 text, AsSpan() PR 10.15 ns 0.92 56 B 0.88
New node from UTF-8 text, Value main 20.07 ns 1.00 96 B 1.00
New node from UTF-8 text, Value PR 19.38 ns 0.97 88 B 0.92
New node from ulong main 10.16 ns 1.00 104 B 1.00
New node from ulong PR 12.04 ns 1.18 88 B 0.85
New node from long main 9.79 ns 1.00 104 B 1.00
New node from long PR 12.59 ns 1.29 88 B 0.85
New node from uint main 10.05 ns 1.00 104 B 1.00
New node from uint PR 12.19 ns 1.21 88 B 0.85
New node from int main 9.96 ns 1.00 104 B 1.00
New node from int PR 12.02 ns 1.21 88 B 0.85
New node from ushort main 10.10 ns 1.00 104 B 1.00
New node from ushort PR 12.05 ns 1.19 88 B 0.85
New node from short main 10.11 ns 1.00 104 B 1.00
New node from short PR 13.11 ns 1.30 88 B 0.85
New node from sbyte main 10.10 ns 1.00 104 B 1.00
New node from sbyte PR 12.41 ns 1.23 88 B 0.85
New node from byte main 10.12 ns 1.00 104 B 1.00
New node from byte PR 12.30 ns 1.22 88 B 0.85
New node from byte, ToByte() main 12.20 ns 1.00 104 B 1.00
New node from byte, ToByte() PR 14.78 ns 1.21 88 B 0.85
New node from sbyte, ToSByte() main 12.20 ns 1.00 104 B 1.00
New node from sbyte, ToSByte() PR 15.22 ns 1.25 88 B 0.85
New node from short, ToInt16() main 12.28 ns 1.00 104 B 1.00
New node from short, ToInt16() PR 15.61 ns 1.27 88 B 0.85
New node from ushort, ToUInt16() main 12.27 ns 1.00 104 B 1.00
New node from ushort, ToUInt16() PR 15.57 ns 1.27 88 B 0.85
New node from int, ToInt32() main 12.49 ns 1.00 104 B 1.00
New node from int, ToInt32() PR 15.05 ns 1.20 88 B 0.85
New node from uint, ToUInt32() main 12.19 ns 1.00 104 B 1.00
New node from uint, ToUInt32() PR 14.63 ns 1.20 88 B 0.85
New node from long, ToInt64() main 12.32 ns 1.00 104 B 1.00
New node from long, ToInt64() PR 15.20 ns 1.23 88 B 0.85
New node from ulong, ToUInt64() main 12.19 ns 1.00 104 B 1.00
New node from ulong, ToUInt64() PR 14.63 ns 1.20 88 B 0.85
New node from int, ToDouble() main 30.71 ns 1.00 104 B 1.00
New node from int, ToDouble() PR 15.22 ns 0.50 88 B 0.85
New node from ulong, AsSpan() main 30.48 ns 1.00 136 B 1.00
New node from ulong, AsSpan() PR 13.63 ns 0.45 88 B 0.65
New node from long, AsSpan() main 30.26 ns 1.00 136 B 1.00
New node from long, AsSpan() PR 13.61 ns 0.45 88 B 0.65
New node from uint, AsSpan() main 30.00 ns 1.00 136 B 1.00
New node from uint, AsSpan() PR 13.50 ns 0.45 88 B 0.65
New node from ushort, AsSpan() main 30.05 ns 1.00 136 B 1.00
New node from ushort, AsSpan() PR 13.36 ns 0.44 88 B 0.65
New node from short, AsSpan() main 30.81 ns 1.00 136 B 1.00
New node from short, AsSpan() PR 13.11 ns 0.43 88 B 0.65
New node from sbyte, AsSpan() main 31.06 ns 1.00 136 B 1.00
New node from sbyte, AsSpan() PR 13.48 ns 0.43 88 B 0.65
New node from byte, AsSpan() main 29.35 ns 1.00 136 B 1.00
New node from byte, AsSpan() PR 13.33 ns 0.45 88 B 0.65
New node from int, AsSpan() main 30.54 ns 1.00 136 B 1.00
New node from int, AsSpan() PR 13.06 ns 0.43 88 B 0.65
New node from ulong, Value main 39.39 ns 1.00 168 B 1.00
New node from ulong, Value PR 24.69 ns 0.63 120 B 0.71
New node from long, Value main 38.97 ns 1.00 168 B 1.00
New node from long, Value PR 23.77 ns 0.61 120 B 0.71
New node from uint, Value main 39.30 ns 1.00 168 B 1.00
New node from uint, Value PR 24.10 ns 0.61 120 B 0.71
New node from ushort, Value main 39.77 ns 1.00 168 B 1.00
New node from ushort, Value PR 25.27 ns 0.64 120 B 0.71
New node from short, Value main 39.66 ns 1.00 168 B 1.00
New node from short, Value PR 24.07 ns 0.61 120 B 0.71
New node from sbyte, Value main 39.93 ns 1.00 168 B 1.00
New node from sbyte, Value PR 23.74 ns 0.59 120 B 0.71
New node from byte, Value main 38.65 ns 1.00 168 B 1.00
New node from byte, Value PR 24.24 ns 0.63 120 B 0.71
New node from int, Value main 39.83 ns 1.00 168 B 1.00
New node from int, Value PR 24.34 ns 0.61 120 B 0.71
Reused node from UTF-8 text, ToByte() main 1.50 ns 1.00 - -
Reused node from UTF-8 text, ToByte() PR 2.21 ns 1.47 - -
Reused node from UTF-8 text, ToSByte() main 1.42 ns 1.00 - -
Reused node from UTF-8 text, ToSByte() PR 2.66 ns 1.88 - -
Reused node from UTF-8 text, ToInt16() main 1.41 ns 1.00 - -
Reused node from UTF-8 text, ToInt16() PR 2.22 ns 1.58 - -
Reused node from UTF-8 text, ToUInt16() main 1.48 ns 1.00 - -
Reused node from UTF-8 text, ToUInt16() PR 2.16 ns 1.46 - -
Reused node from UTF-8 text, ToInt32() main 1.87 ns 1.00 - -
Reused node from UTF-8 text, ToInt32() PR 2.78 ns 1.49 - -
Reused node from UTF-8 text, ToUInt32() main 1.43 ns 1.00 - -
Reused node from UTF-8 text, ToUInt32() PR 2.55 ns 1.78 - -
Reused node from UTF-8 text, ToInt64() main 1.62 ns 1.00 - -
Reused node from UTF-8 text, ToInt64() PR 2.17 ns 1.34 - -
Reused node from UTF-8 text, ToUInt64() main 1.62 ns 1.00 - -
Reused node from UTF-8 text, ToUInt64() PR 16.07 ns 9.93 - -
Reused node from UTF-8 text, ToDouble() main 11.65 ns 1.00 - -
Reused node from UTF-8 text, ToDouble() PR 2.38 ns 0.20 - -
Reused node from UTF-8 text, AsSpan() main 2.07 ns 1.00 - -
Reused node from UTF-8 text, AsSpan() PR 1.56 ns 0.76 - -
Reused node from UTF-8 text, Value main 10.56 ns 1.00 32 B 1.00
Reused node from UTF-8 text, Value PR 10.05 ns 0.95 32 B 1.00
Reused node from byte, ToByte() main 1.43 ns 1.00 - -
Reused node from byte, ToByte() PR 1.42 ns 0.99 - -
Reused node from sbyte, ToSByte() main 1.41 ns 1.00 - -
Reused node from sbyte, ToSByte() PR 1.72 ns 1.22 - -
Reused node from short, ToInt16() main 1.49 ns 1.00 - -
Reused node from short, ToInt16() PR 1.48 ns 1.00 - -
Reused node from ushort, ToUInt16() main 1.43 ns 1.00 - -
Reused node from ushort, ToUInt16() PR 1.41 ns 0.99 - -
Reused node from int, ToInt32() main 2.17 ns 1.00 - -
Reused node from int, ToInt32() PR 1.94 ns 0.89 - -
Reused node from uint, ToUInt32() main 1.51 ns 1.00 - -
Reused node from uint, ToUInt32() PR 1.77 ns 1.17 - -
Reused node from long, ToInt64() main 1.61 ns 1.00 - -
Reused node from long, ToInt64() PR 1.36 ns 0.85 - -
Reused node from ulong, ToUInt64() main 1.62 ns 1.00 - -
Reused node from ulong, ToUInt64() PR 1.23 ns 0.76 - -
Reused node from int, ToDouble() main 19.14 ns 1.00 - -
Reused node from int, ToDouble() PR 1.64 ns 0.09 - -
Reused node from ulong, AsSpan() main 2.15 ns 1.00 - -
Reused node from ulong, AsSpan() PR 0.62 ns 0.29 - -
Reused node from long, AsSpan() main 2.15 ns 1.00 - -
Reused node from long, AsSpan() PR 0.71 ns 0.33 - -
Reused node from uint, AsSpan() main 2.16 ns 1.00 - -
Reused node from uint, AsSpan() PR 0.59 ns 0.27 - -
Reused node from ushort, AsSpan() main 2.15 ns 1.00 - -
Reused node from ushort, AsSpan() PR 0.75 ns 0.35 - -
Reused node from short, AsSpan() main 2.15 ns 1.00 - -
Reused node from short, AsSpan() PR 0.54 ns 0.25 - -
Reused node from sbyte, AsSpan() main 2.15 ns 1.00 - -
Reused node from sbyte, AsSpan() PR 0.58 ns 0.27 - -
Reused node from byte, AsSpan() main 2.15 ns 1.00 - -
Reused node from byte, AsSpan() PR 0.58 ns 0.27 - -
Reused node from int, AsSpan() main 2.16 ns 1.00 - -
Reused node from int, AsSpan() PR 0.53 ns 0.25 - -
Reused node from ulong, Value main 10.44 ns 1.00 32 B 1.00
Reused node from ulong, Value PR 9.82 ns 0.94 32 B 1.00
Reused node from long, Value main 10.34 ns 1.00 32 B 1.00
Reused node from long, Value PR 9.38 ns 0.91 32 B 1.00
Reused node from uint, Value main 10.84 ns 1.00 32 B 1.00
Reused node from uint, Value PR 10.00 ns 0.92 32 B 1.00
Reused node from int, Value main 10.90 ns 1.00 32 B 1.00
Reused node from int, Value PR 9.81 ns 0.90 32 B 1.00
Reused node from ushort, Value main 10.72 ns 1.00 32 B 1.00
Reused node from ushort, Value PR 9.58 ns 0.89 32 B 1.00
Reused node from short, Value main 9.95 ns 1.00 32 B 1.00
Reused node from short, Value PR 9.57 ns 0.96 32 B 1.00
Reused node from sbyte, Value main 10.56 ns 1.00 32 B 1.00
Reused node from sbyte, Value PR 9.85 ns 0.93 32 B 1.00
Reused node from byte, Value main 10.79 ns 1.00 32 B 1.00
Reused node from byte, Value PR 9.69 ns 0.90 32 B 1.00

Verdict: a net improvement. On the hot path, Hot Chocolate coerces each integer variable 32% faster with 46% less allocation, because the node no longer allocates a byte[] on its first conversion, and Fusion forwards each integer variable 8 B smaller at the same speed. Cost analysis reads literal slicing arguments as doubles 5 times faster, because the text is parsed as a long first. A reused node parses its text on every conversion, which costs about 1 ns more per call and only reaches the hot path for arguments that mix literals and variables. Building a node from an int costs 2.5 ns more because the text is formatted eagerly, and allocates 16 B less; printing or forwarding such a node is 2 to 3 times faster. The reused ToUInt64() row is an artifact of the long pass: the out-of-process pass measured 2.07 ns against 0.89 ns on main, in line with the other widths.

FloatValueNode

Method Version Mean Ratio Allocated Alloc Ratio
New node from UTF-8 text main 10.41 ns 1.00 72 B 1.00
New node from UTF-8 text PR 9.71 ns 0.93 72 B 1.00
New node from UTF-8 text, ToDouble() main 32.72 ns 1.00 120 B 1.00
New node from UTF-8 text, ToDouble() PR 28.33 ns 0.87 72 B 0.60
New node from UTF-8 text, ToDecimal() main 28.79 ns 1.00 120 B 1.00
New node from UTF-8 text, ToDecimal() PR 23.14 ns 0.80 72 B 0.60
New node from UTF-8 text, AsSpan() main 10.71 ns 1.00 72 B 1.00
New node from UTF-8 text, AsSpan() PR 9.13 ns 0.85 72 B 1.00
New node from UTF-8 text, Value main 20.97 ns 1.00 104 B 1.00
New node from UTF-8 text, Value PR 24.31 ns 1.16 104 B 1.00
New node from decimal main 10.99 ns 1.00 120 B 1.00
New node from decimal PR 42.25 ns 3.84 104 B 0.87
New node from double main 11.23 ns 1.00 120 B 1.00
New node from double PR 6.71 ns 0.60 72 B 0.60
New node from decimal, ToDouble() main 14.43 ns 1.00 120 B 1.00
New node from decimal, ToDouble() PR 55.83 ns 3.87 104 B 0.87
New node from double, ToDouble() main 13.10 ns 1.00 120 B 1.00
New node from double, ToDouble() PR 7.49 ns 0.57 72 B 0.60
New node from double, ToDecimal() main 20.14 ns 1.00 120 B 1.00
New node from double, ToDecimal() PR 11.82 ns 0.59 72 B 0.60
New node from double, AsSpan() main 60.60 ns 1.00 152 B 1.00
New node from double, AsSpan() PR 52.65 ns 0.87 104 B 0.68
New node from decimal, ToDecimal() main 14.26 ns 1.00 120 B 1.00
New node from decimal, ToDecimal() PR 51.28 ns 3.60 104 B 0.87
New node from decimal, AsSpan() main 43.78 ns 1.00 152 B 1.00
New node from decimal, AsSpan() PR 32.59 ns 0.74 104 B 0.68
New node from decimal, Value main 59.48 ns 1.00 184 B 1.00
New node from decimal, Value PR 48.20 ns 0.81 136 B 0.74
New node from double, Value main 72.71 ns 1.00 184 B 1.00
New node from double, Value PR 61.21 ns 0.84 136 B 0.74
Reused node from UTF-8 text, ToDouble() main 1.54 ns 1.00 - -
Reused node from UTF-8 text, ToDouble() PR 0.29 ns 0.19 - -
Reused node from UTF-8 text, ToDecimal() main 2.41 ns 1.00 - -
Reused node from UTF-8 text, ToDecimal() PR 13.15 ns 5.46 - -
Reused node from UTF-8 text, AsSpan() main 1.86 ns 1.00 - -
Reused node from UTF-8 text, AsSpan() PR 1.70 ns 0.92 - -
Reused node from UTF-8 text, Value main 10.80 ns 1.00 32 B 1.00
Reused node from UTF-8 text, Value PR 10.89 ns 1.01 32 B 1.00
Reused node from decimal, ToDouble() main 2.89 ns 1.00 - -
Reused node from decimal, ToDouble() PR 13.84 ns 4.79 - -
Reused node from double, ToDouble() main 1.49 ns 1.00 - -
Reused node from double, ToDouble() PR 0.33 ns 0.22 - -
Reused node from double, ToDecimal() main 6.05 ns 1.00 - -
Reused node from double, ToDecimal() PR 4.13 ns 0.68 - -
Reused node from decimal, ToDecimal() main 2.24 ns 1.00 - -
Reused node from decimal, ToDecimal() PR 12.74 ns 5.68 - -
Reused node from decimal, AsSpan() main 2.15 ns 1.00 - -
Reused node from decimal, AsSpan() PR 1.16 ns 0.54 - -
Reused node from double, AsSpan() main 2.14 ns 1.00 - -
Reused node from double, AsSpan() PR 1.14 ns 0.53 - -
Reused node from decimal, Value main 11.05 ns 1.00 32 B 1.00
Reused node from decimal, Value PR 9.82 ns 0.89 32 B 1.00
Reused node from double, Value main 10.90 ns 1.00 32 B 1.00
Reused node from double, Value PR 9.75 ns 0.89 32 B 1.00

Verdict: a net improvement on the hot path. The node stays at 72 B, so Fusion forwards float variables unchanged, Hot Chocolate coerces each float variable 13% faster with 40% less allocation, and a reused node returns its cached double in 0.3 ns instead of parsing. Nodes built from double are faster and smaller on every operation. Nodes built from decimal, which only DecimalType.ValueToLiteral creates, format their text eagerly and parse it back on conversion, which costs 3 to 4 times more on those paths. ToDecimal() on a reused text node parses on every call, 11 ns more, because the node has no room for a cached decimal. The Value row on a new text node shows +16% in the long pass and -3% in the out-of-process pass, so it is noise.

StringValueNode

Method Version Mean Ratio Allocated Alloc Ratio
New node from UTF-8 text main 9.38 ns 1.00 72 B 1.00
New node from UTF-8 text PR 9.16 ns 0.98 72 B 1.00
New node from UTF-8 text, AsSpan() main 10.91 ns 1.00 72 B 1.00
New node from UTF-8 text, AsSpan() PR 10.10 ns 0.93 72 B 1.00
New node from UTF-8 text, Value main 22.82 ns 1.00 112 B 1.00
New node from UTF-8 text, Value PR 23.64 ns 1.04 112 B 1.00
New node from UTF-8 text, Value then AsSpan() main 25.75 ns 1.00 112 B 1.00
New node from UTF-8 text, Value then AsSpan() PR 25.39 ns 0.99 112 B 1.00
New node from string main 5.56 ns 1.00 72 B 1.00
New node from string PR 6.31 ns 1.13 72 B 1.00
New node from string, AsSpan() main 28.93 ns 1.00 104 B 1.00
New node from string, AsSpan() PR 23.05 ns 0.80 104 B 1.00
New node from string, Value main 6.92 ns 1.00 72 B 1.00
New node from string, Value PR 7.82 ns 1.13 72 B 1.00
New node from string, Value then AsSpan() main 29.26 ns 1.00 104 B 1.00
New node from string, Value then AsSpan() PR 24.29 ns 0.83 104 B 1.00
Reused node from UTF-8 text, AsSpan() main 2.47 ns 1.00 - -
Reused node from UTF-8 text, AsSpan() PR 1.74 ns 0.71 - -
Reused node from UTF-8 text, Value main 1.34 ns 1.00 - -
Reused node from UTF-8 text, Value PR 1.59 ns 1.18 - -
Reused node from string, AsSpan() main 2.68 ns 1.00 - -
Reused node from string, AsSpan() PR 1.15 ns 0.43 - -
Reused node from string, Value main 1.25 ns 1.00 - -
Reused node from string, Value PR 1.43 ns 1.15 - -

Verdict: neutral to positive. Node size is unchanged, construction and Value are within noise, and encoding a node built from a string is 20% faster on the first call and 57% faster on a reused node. The two reused Value rows differ by 0.2 ns in the long pass and are equal in the out-of-process pass.

EnumValueNode

Method Version Mean Ratio Allocated Alloc Ratio
New node from UTF-8 text main 9.39 ns 1.00 64 B 1.00
New node from UTF-8 text PR 9.57 ns 1.02 64 B 1.00
New node from UTF-8 text, AsSpan() main 9.73 ns 1.00 64 B 1.00
New node from UTF-8 text, AsSpan() PR 10.41 ns 1.07 64 B 1.00
New node from UTF-8 text, Value main 22.36 ns 1.00 112 B 1.00
New node from UTF-8 text, Value PR 22.12 ns 0.99 112 B 1.00
New node from enum value main 25.58 ns 1.00 136 B 1.00
New node from enum value PR 25.24 ns 0.99 136 B 1.00
New node from string main 4.93 ns 1.00 64 B 1.00
New node from string PR 7.77 ns 1.57 64 B 1.00
New node from enum value, AsSpan() main 45.69 ns 1.00 176 B 1.00
New node from enum value, AsSpan() PR 45.71 ns 1.00 208 B 1.18
New node from string, Value main 6.50 ns 1.00 64 B 1.00
New node from string, Value PR 11.00 ns 1.69 64 B 1.00
New node from string, AsSpan() main 25.41 ns 1.00 104 B 1.00
New node from string, AsSpan() PR 26.16 ns 1.03 136 B 1.31
New node from enum value, Value main 27.74 ns 1.00 136 B 1.00
New node from enum value, Value PR 27.46 ns 0.99 136 B 1.00
Reused node from UTF-8 text, AsSpan() main 1.19 ns 1.00 - -
Reused node from UTF-8 text, AsSpan() PR 1.20 ns 1.01 - -
Reused node from UTF-8 text, Value main 1.37 ns 1.00 - -
Reused node from UTF-8 text, Value PR 0.80 ns 0.58 - -
Reused node from enum value, AsSpan() main 1.88 ns 1.00 - -
Reused node from enum value, AsSpan() PR 2.15 ns 1.14 - -
Reused node from string, AsSpan() main 1.88 ns 1.00 - -
Reused node from string, AsSpan() PR 2.15 ns 1.14 - -
Reused node from enum value, Value main 1.27 ns 1.00 - -
Reused node from enum value, Value PR 0.79 ns 0.62 - -
Reused node from string, Value main 1.25 ns 1.00 - -
Reused node from string, Value PR 0.54 ns 0.43 - -

Verdict: neutral on the hot path. Every node stays at 64 B, Fusion forwards enum variables unchanged, and reading Value on a reused node is twice as fast. The two "from string" construction rows did not reproduce in the out-of-process pass (7.07 ns on main against 6.46 ns, and 8.34 ns against 8.91 ns with Value): the remaining 0.6 ns on Value is one type check that the JIT folds away on main only when construction and read sit in the same method, which they do not in production. Encoding a node built from a string allocates a 32 B holder once; no request path does that. Nodes built from an enum value are equal on construction and Value, and encoding one allocates the same 32 B holder.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Patch coverage

83.2% of changed lines covered (114/137)

File Covered Changed Patch %
…/Language/src/Language.SyntaxTree/IntValueNode.cs 41 59 69.5% 🔴
…/Language/src/Language.SyntaxTree/FloatValueNode.cs 42 47 89.4% 🟡
…/Language/src/Language.SyntaxTree/EnumValueNode.cs 24 24 100.0% 🟢
…/Language/src/Language.SyntaxTree/StringValueNode.cs 6 6 100.0% 🟢
…/Language/src/Language.SyntaxTree/Utilities/ThrowHelper.cs 1 1 100.0% 🟢
Uncovered changed lines (JSON)
{
  "sha": "9e86fabffc18844b568d59945ac66a9fa6993ceb",
  "files": [
    { "path": "src/HotChocolate/Language/src/Language.SyntaxTree/IntValueNode.cs", "ranges": [[326, 326], [339, 339], [365, 365], [378, 378], [391, 391], [404, 404], [417, 417], [428, 428], [430, 430], [432, 432], [435, 435], [437, 437], [440, 440], [455, 455], [457, 457], [460, 460], [477, 477], [501, 501]] },
    { "path": "src/HotChocolate/Language/src/Language.SyntaxTree/FloatValueNode.cs", "ranges": [[202, 202], [207, 207], [231, 231], [249, 249], [254, 254]] }
  ]
}

Project coverage: 58.6% (314039/536107 lines)

@tobias-tengler
tobias-tengler force-pushed the tte/fix-value-node-concurrent-reads branch from 8534cd5 to d6609e5 Compare October 9, 2026 10:26
FloatValueNode stores double and decimal values inline and formats them
on first read instead of in the constructor. StringValueNode publishes
its lazily encoded segment through a volatile flag instead of an extra
field.
FloatValueNode stores one inline double and a volatile state byte that fits in
padding, so the node stays 72 B. Text nodes cache the parsed double on the
first ToDouble, double nodes format lazily and publish the segment through the
state, decimal nodes format eagerly and parse on conversion.

EnumValueNode keeps one object field that holds the string, or a holder with
the string and its UTF-8 bytes once a string node is encoded, so the node
stays 64 B and text nodes are unchanged.

IntValueNode parses floating point conversions as long first and throws
through ThrowHelper when the text is not numeric.
@tobias-tengler
tobias-tengler force-pushed the tte/fix-value-node-concurrent-reads branch from 67984b1 to 620a8ed Compare October 10, 2026 16:50
FloatValueNode no longer stores State.Text in its text constructor, since a
new node starts in that state. StringValueNode sets its segment flag
unconditionally instead of evaluating IsEmpty. EnumValueNode moves the encode
fallback of AsSpan and AsMemorySegment into a non-inlined helper so that the
fast path inlines into callers.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IntValueNode.Value is not thread-safe on first read, producing empty literals and invalid SDL

1 participant