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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1def1a5df451e3c7b1afa22248c2c54b2b135f5712eb19a989b41209a8c5622
4
- data.tar.gz: 16a9741bbda86c16c78aafec91688d962c9ae58e9b032681a07d7b633497c258
3
+ metadata.gz: 2533a42fd8e3080a107eb28117106080a950076630082392805c6a29d86ab7d8
4
+ data.tar.gz: 77cf546b4d9b69b22370e8e8a5b6eec12fff1b8122a4b598b11a0a077953f2ec
5
5
  SHA512:
6
- metadata.gz: 2be36b8b49c5b7c6d6525364c6f10a24d696eb40d8dae2343bdf1b1017e2f2fd47e6c52d8cdf200b4f1cf22a75fdd0ea41bf217e533090a9a42fa725ee7018a8
7
- data.tar.gz: 5a4fe53c914d211714bbd2a31d44af37743edf1007f4599f52c6dfcce9e3bf970d90778588b1ee17267394017ed554752882f63d45a06fa7ce693786b97dc152
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(args, kwargs, result)
1085
+ snapshot = SpanMeta::Snapshot.capture(kept_args, kept_kwargs, kept_result)
1085
1086
  unless snapshot
1086
- input_meta = SpanMeta.build_input_meta(args, kwargs, human_inputs)
1087
- output_meta = SpanMeta.build_output_meta(result, human_output)
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
@@ -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
- result = serialize_value_inner(value, 0, dropped)
49
- if oversized?(result)
50
- dropped << "too_large"
51
- return [unserializable_stub(value, "too_large"), dropped]
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
- def serialize_value_inner(value, depth, dropped = [])
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, String
116
+ when nil, true, false, Integer
76
117
  value
118
+ when String
119
+ counted_string(budget, value)
77
120
  when Hash
78
- value.each_with_object({}) do |(k, v), acc|
79
- acc[safe_to_s(k)] = serialize_value_inner(v, depth + 1, dropped)
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
- unserializable_stub(value, "inner_error")
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
- serialized = args.map do |arg|
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bitfab
4
- VERSION = "0.74.1"
4
+ VERSION = "0.74.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitfab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.74.1
4
+ version: 0.74.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harvest Team