bitfab 0.74.1 → 0.74.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/bitfab/client.rb +5 -4
- data/lib/bitfab/serialize.rb +135 -27
- data/lib/bitfab/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2533a42fd8e3080a107eb28117106080a950076630082392805c6a29d86ab7d8
|
|
4
|
+
data.tar.gz: 77cf546b4d9b69b22370e8e8a5b6eec12fff1b8122a4b598b11a0a077953f2ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cef1f28295d0d4c140bd23b003bb712d460a10fe9225ce730526159c083e20a2bf8a423d4641eeffb369b4a6509b0ef86cbfc992ae5d1f5641c91594d0d0868f
|
|
7
|
+
data.tar.gz: 99f81dace42263bf6e770451cbe266ec5e4a5566ecc559e86984d87f7d82f67c214a610a74f358ec188d2dd52383ef84c4f4e4c4be52cf2ce609f7ec818febcc
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -1069,8 +1069,9 @@ module Bitfab
|
|
|
1069
1069
|
span_data.merge!(extra_span_data.except(*SimulationPlan::CONTENT_KEYS))
|
|
1070
1070
|
span_data["content_off_by_simulation_plan"] = true
|
|
1071
1071
|
else
|
|
1072
|
-
human_inputs, input_dropped = Serialize.serialize_inputs_with_report(args, kwargs)
|
|
1072
|
+
human_inputs, input_dropped, (kept_args, kept_kwargs) = Serialize.serialize_inputs_with_report(args, kwargs)
|
|
1073
1073
|
human_output, output_dropped = Serialize.serialize_value_with_report(result)
|
|
1074
|
+
kept_result = Serialize.too_large?(output_dropped) ? human_output : result
|
|
1074
1075
|
dropped = input_dropped + output_dropped
|
|
1075
1076
|
|
|
1076
1077
|
span_data = {
|
|
@@ -1081,10 +1082,10 @@ module Bitfab
|
|
|
1081
1082
|
"function_name" => function_name
|
|
1082
1083
|
}
|
|
1083
1084
|
span_data.merge!(extra_span_data)
|
|
1084
|
-
snapshot = SpanMeta::Snapshot.capture(
|
|
1085
|
+
snapshot = SpanMeta::Snapshot.capture(kept_args, kept_kwargs, kept_result)
|
|
1085
1086
|
unless snapshot
|
|
1086
|
-
input_meta = SpanMeta.build_input_meta(
|
|
1087
|
-
output_meta = SpanMeta.build_output_meta(
|
|
1087
|
+
input_meta = SpanMeta.build_input_meta(kept_args, kept_kwargs, human_inputs)
|
|
1088
|
+
output_meta = SpanMeta.build_output_meta(kept_result, human_output)
|
|
1088
1089
|
span_data["input_meta"] = input_meta if input_meta
|
|
1089
1090
|
span_data["output_meta"] = output_meta if output_meta
|
|
1090
1091
|
end
|
data/lib/bitfab/serialize.rb
CHANGED
|
@@ -20,6 +20,26 @@ module Bitfab
|
|
|
20
20
|
# Recursion guard for cyclic graphs and pathologically nested structures.
|
|
21
21
|
MAX_SERIALIZE_DEPTH = 16
|
|
22
22
|
|
|
23
|
+
# A running count of bytes the JSON encoding of the walk's output is known
|
|
24
|
+
# to need (`used`), and of every byte ever counted, including counts later
|
|
25
|
+
# taken back when a part that raised was stubbed (`charged`). It is also
|
|
26
|
+
# the catch tag the walk throws to once `used` passes MAX_SERIALIZED_BYTES.
|
|
27
|
+
Budget = Struct.new(:used, :charged)
|
|
28
|
+
|
|
29
|
+
# Once this many bytes have been counted in total, counting stops and the
|
|
30
|
+
# walk finishes uncounted, so parts that raise and are stubbed over and
|
|
31
|
+
# over cannot make the walk do unbounded work before the size check.
|
|
32
|
+
MAX_COUNTED_BYTES = 4 * MAX_SERIALIZED_BYTES
|
|
33
|
+
|
|
34
|
+
# String encodings whose bytes JSON writes out one for one (or rejects). In
|
|
35
|
+
# any other encoding a character can take more bytes than its UTF-8 form,
|
|
36
|
+
# so only the character count is a safe floor there.
|
|
37
|
+
BYTE_FOR_BYTE_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::BINARY].freeze
|
|
38
|
+
|
|
39
|
+
# The dropped entry for a value replaced by a placeholder because it is
|
|
40
|
+
# past MAX_SERIALIZED_BYTES.
|
|
41
|
+
TOO_LARGE = "too_large"
|
|
42
|
+
|
|
23
43
|
module_function
|
|
24
44
|
|
|
25
45
|
# Serialize a value for JSON storage (human-readable).
|
|
@@ -43,22 +63,43 @@ module Bitfab
|
|
|
43
63
|
# non-empty dropped means the capture is lossy, so the send boundary marks
|
|
44
64
|
# the span serialization_degraded (non-replayable) instead of shipping it as
|
|
45
65
|
# if it round-trips. Mirrors the Python/TS SDKs' report serializers.
|
|
66
|
+
#
|
|
67
|
+
# The walk counts a lower bound on the JSON size of what it has built so
|
|
68
|
+
# far and gives up as soon as that passes MAX_SERIALIZED_BYTES, so a value
|
|
69
|
+
# holding huge arrays or strings is stubbed without first being copied and
|
|
70
|
+
# encoded in full. The count never exceeds the real size, so a value that
|
|
71
|
+
# fits is never stubbed; the JSON.dump check afterwards still catches values
|
|
72
|
+
# whose real size passes the cap while the count does not.
|
|
46
73
|
def serialize_value_with_report(value)
|
|
47
74
|
dropped = []
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
75
|
+
budget = Budget.new(1, 1)
|
|
76
|
+
result = catch(budget) { serialize_value_inner(value, 0, dropped, budget) }
|
|
77
|
+
reason = if budget.used > MAX_SERIALIZED_BYTES
|
|
78
|
+
"too_large_over_#{MAX_SERIALIZED_BYTES}_bytes"
|
|
79
|
+
elsif oversized?(result)
|
|
80
|
+
"too_large"
|
|
52
81
|
end
|
|
53
|
-
[result, dropped]
|
|
82
|
+
return [result, dropped] unless reason
|
|
83
|
+
|
|
84
|
+
dropped << TOO_LARGE
|
|
85
|
+
[unserializable_stub(value, reason), dropped]
|
|
54
86
|
rescue StandardError, SystemStackError
|
|
55
87
|
[unserializable_stub(value, "unexpected_error"), [class_name(value)]]
|
|
56
88
|
end
|
|
57
89
|
|
|
58
|
-
|
|
90
|
+
# `budget`, when given, counts the bytes this call's output needs beyond the
|
|
91
|
+
# one byte its caller already counted for it (every JSON value takes at
|
|
92
|
+
# least one), and throws to the budget once the total passes the cap.
|
|
93
|
+
# A hash entry costs its quoted key, a colon, a comma or brace, and the
|
|
94
|
+
# value's byte. An array or set element costs its byte plus a comma or
|
|
95
|
+
# bracket; a plain Array or Set is charged for every element before any is
|
|
96
|
+
# copied, so a huge one is turned away at once. When a part raises, its
|
|
97
|
+
# stub replaces whatever that part had counted.
|
|
98
|
+
def serialize_value_inner(value, depth, dropped = [], budget = nil)
|
|
99
|
+
mark = budget&.used
|
|
59
100
|
if depth > MAX_SERIALIZE_DEPTH
|
|
60
101
|
dropped << "max_depth"
|
|
61
|
-
return "<unserializable: max_depth>"
|
|
102
|
+
return counted_string(budget, "<unserializable: max_depth>")
|
|
62
103
|
end
|
|
63
104
|
|
|
64
105
|
case value
|
|
@@ -70,47 +111,95 @@ module Bitfab
|
|
|
70
111
|
value
|
|
71
112
|
else
|
|
72
113
|
dropped << "non_finite_float"
|
|
73
|
-
"<unserializable: non_finite_float>"
|
|
114
|
+
counted_string(budget, "<unserializable: non_finite_float>")
|
|
74
115
|
end
|
|
75
|
-
when nil, true, false, Integer
|
|
116
|
+
when nil, true, false, Integer
|
|
76
117
|
value
|
|
118
|
+
when String
|
|
119
|
+
counted_string(budget, value)
|
|
77
120
|
when Hash
|
|
78
|
-
value
|
|
79
|
-
|
|
121
|
+
serialize_hash_inner(value, depth, dropped, budget)
|
|
122
|
+
when Array, Set
|
|
123
|
+
plain = value.instance_of?(Array) || value.instance_of?(Set)
|
|
124
|
+
charge(budget, 2 * value.size) if plain
|
|
125
|
+
value.map do |v|
|
|
126
|
+
charge(budget, 2) unless plain
|
|
127
|
+
serialize_value_inner(v, depth + 1, dropped, budget)
|
|
80
128
|
end
|
|
81
|
-
when Array
|
|
82
|
-
value.map { |v| serialize_value_inner(v, depth + 1, dropped) }
|
|
83
|
-
when Set
|
|
84
|
-
value.map { |v| serialize_value_inner(v, depth + 1, dropped) }
|
|
85
129
|
when Time, DateTime
|
|
86
|
-
safely_call(value, "iso8601", 3) { safe_to_s(value) }
|
|
130
|
+
counted_string(budget, safely_call(value, "iso8601", 3) { safe_to_s(value) })
|
|
87
131
|
when Date
|
|
88
|
-
safe_to_s(value)
|
|
132
|
+
counted_string(budget, safe_to_s(value))
|
|
89
133
|
when Symbol
|
|
90
|
-
value.to_s
|
|
134
|
+
counted_string(budget, value.to_s)
|
|
91
135
|
else
|
|
92
136
|
if value.respond_to?(:to_h)
|
|
93
137
|
h = safely_call(value, "to_h") do
|
|
94
138
|
dropped << class_name(value)
|
|
95
|
-
return unserializable_stub(value, "to_h_raised")
|
|
139
|
+
return counted_string(budget, unserializable_stub(value, "to_h_raised"))
|
|
96
140
|
end
|
|
97
|
-
serialize_value_inner(h, depth + 1, dropped)
|
|
141
|
+
serialize_value_inner(h, depth + 1, dropped, budget)
|
|
98
142
|
elsif value.respond_to?(:to_a)
|
|
99
143
|
a = safely_call(value, "to_a") do
|
|
100
144
|
dropped << class_name(value)
|
|
101
|
-
return unserializable_stub(value, "to_a_raised")
|
|
145
|
+
return counted_string(budget, unserializable_stub(value, "to_a_raised"))
|
|
102
146
|
end
|
|
103
|
-
serialize_value_inner(a, depth + 1, dropped)
|
|
147
|
+
serialize_value_inner(a, depth + 1, dropped, budget)
|
|
104
148
|
else
|
|
105
149
|
# An arbitrary object with no structured conversion: stringifying it is
|
|
106
150
|
# a lossy capture (a repr, not its data), so report it as dropped.
|
|
107
151
|
dropped << class_name(value)
|
|
108
|
-
safe_to_s(value)
|
|
152
|
+
counted_string(budget, safe_to_s(value))
|
|
109
153
|
end
|
|
110
154
|
end
|
|
111
155
|
rescue StandardError, SystemStackError
|
|
112
156
|
dropped << class_name(value)
|
|
113
|
-
|
|
157
|
+
budget.used = mark if budget
|
|
158
|
+
counted_string(budget, unserializable_stub(value, "inner_error"))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Serialize a hash's entries into a plain Hash keyed by each key's string.
|
|
162
|
+
# Keys that stringify alike (such as "data" and :data) share one output
|
|
163
|
+
# key holding the last of their values, so each output key is counted
|
|
164
|
+
# once, only the value that is kept is walked, and the key keeps the place
|
|
165
|
+
# of its first occurrence.
|
|
166
|
+
def serialize_hash_inner(hash, depth, dropped, budget)
|
|
167
|
+
entries = []
|
|
168
|
+
last_index = {}
|
|
169
|
+
hash.each do |k, v|
|
|
170
|
+
key = safe_to_s(k)
|
|
171
|
+
charge(budget, string_floor(key) + 5) unless last_index.key?(key)
|
|
172
|
+
last_index[key] = entries.size
|
|
173
|
+
entries << [key, v]
|
|
174
|
+
end
|
|
175
|
+
entries.each_with_index.with_object({}) do |((key, v), index), acc|
|
|
176
|
+
acc[key] = (last_index[key] == index) ? serialize_value_inner(v, depth + 1, dropped, budget) : nil
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Add `bytes` to the running count and stop the walk once it passes the cap.
|
|
181
|
+
def charge(budget, bytes)
|
|
182
|
+
return unless budget && budget.charged <= MAX_COUNTED_BYTES
|
|
183
|
+
|
|
184
|
+
budget.used += bytes
|
|
185
|
+
budget.charged += bytes
|
|
186
|
+
throw budget if budget.used > MAX_SERIALIZED_BYTES
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Charge for a string the walk emits (its quotes beyond the byte the caller
|
|
190
|
+
# counted) and return it.
|
|
191
|
+
def counted_string(budget, str)
|
|
192
|
+
charge(budget, string_floor(str) + 1)
|
|
193
|
+
str
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# The fewest bytes JSON can write for the characters of `str`, quotes
|
|
197
|
+
# aside. A String subclass may encode itself through its own to_json, so it
|
|
198
|
+
# counts as nothing.
|
|
199
|
+
def string_floor(str)
|
|
200
|
+
return 0 unless str.instance_of?(String)
|
|
201
|
+
|
|
202
|
+
BYTE_FOR_BYTE_ENCODINGS.include?(str.encoding) ? str.bytesize : str.length
|
|
114
203
|
end
|
|
115
204
|
|
|
116
205
|
# Call `value.<method>` and return the result, or yield to the fallback
|
|
@@ -153,23 +242,42 @@ module Bitfab
|
|
|
153
242
|
end
|
|
154
243
|
|
|
155
244
|
# Like serialize_inputs, but also returns the accumulated dropped list so the
|
|
156
|
-
# send boundary can mark a lossy capture non-replayable.
|
|
245
|
+
# send boundary can mark a lossy capture non-replayable. The third element
|
|
246
|
+
# is [args, kwargs] with every value past MAX_SERIALIZED_BYTES swapped for
|
|
247
|
+
# its placeholder (the originals when none is), so the type-preserving
|
|
248
|
+
# capture never copies a value the readable capture already gave up on.
|
|
157
249
|
def serialize_inputs_with_report(args, kwargs = {})
|
|
158
250
|
dropped = []
|
|
159
|
-
|
|
251
|
+
kept_args = args
|
|
252
|
+
serialized = args.each_with_index.map do |arg, index|
|
|
160
253
|
result, arg_dropped = serialize_value_with_report(arg)
|
|
161
254
|
dropped.concat(arg_dropped)
|
|
255
|
+
if too_large?(arg_dropped)
|
|
256
|
+
kept_args = kept_args.dup if kept_args.equal?(args)
|
|
257
|
+
kept_args[index] = result
|
|
258
|
+
end
|
|
162
259
|
result
|
|
163
260
|
end
|
|
261
|
+
kept_kwargs = kwargs
|
|
164
262
|
unless kwargs.empty?
|
|
165
263
|
kw = kwargs.each_with_object({}) do |(k, v), acc|
|
|
166
264
|
result, v_dropped = serialize_value_with_report(v)
|
|
167
265
|
dropped.concat(v_dropped)
|
|
266
|
+
if too_large?(v_dropped)
|
|
267
|
+
kept_kwargs = kept_kwargs.dup if kept_kwargs.equal?(kwargs)
|
|
268
|
+
kept_kwargs[k] = result
|
|
269
|
+
end
|
|
168
270
|
acc[safe_to_s(k)] = result
|
|
169
271
|
end
|
|
170
272
|
serialized << kw
|
|
171
273
|
end
|
|
172
|
-
[serialized, dropped]
|
|
274
|
+
[serialized, dropped, [kept_args, kept_kwargs]]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Whether a dropped list from serialize_value_with_report means the value
|
|
278
|
+
# itself was replaced by a placeholder for being past MAX_SERIALIZED_BYTES.
|
|
279
|
+
def too_large?(dropped)
|
|
280
|
+
dropped.include?(TOO_LARGE)
|
|
173
281
|
end
|
|
174
282
|
|
|
175
283
|
# Marshal a value to a Base64-encoded string for Ruby-to-Ruby reconstruction.
|
data/lib/bitfab/version.rb
CHANGED