bitfab 0.67.3 → 0.68.0
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 +9 -7
- data/lib/bitfab/experiments.rb +9 -0
- data/lib/bitfab/http_client.rb +11 -6
- data/lib/bitfab/otel.rb +172 -37
- data/lib/bitfab/replay.rb +1 -2
- data/lib/bitfab/seed.rb +1 -1
- data/lib/bitfab/serialize.rb +10 -25
- data/lib/bitfab/span_meta.rb +635 -0
- data/lib/bitfab/version.rb +1 -1
- data/lib/bitfab.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c05530ec639d246b474f368fae48df4ed2230e4275272f2d903c9ef81bc96b6b
|
|
4
|
+
data.tar.gz: 1019089f79f07cafb3b0e8be5604717ed32a1a894f28a7ae6b26d71cf05c1889
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 551641d9eb87d4c98d16d17cf5f6dcfe3f795f84a8fe1c155ba14d5db41bb5e53b04229ce0424ecac59430f854e79790c1460fb9fbc7ba10a2e0cb2bf739acd4
|
|
7
|
+
data.tar.gz: 28c329c02e65d57a75af5ae12895be94bc525dfa6ef01e2203a7485e3998a5d6c0e04209e2e78694f6085777bdad2009dba36c6d8ee4b7e75bb95bf8d98e6f0f
|
data/lib/bitfab/client.rb
CHANGED
|
@@ -1039,6 +1039,7 @@ module Bitfab
|
|
|
1039
1039
|
content_off = !declared_node && (extra_span_data["content_off_by_simulation_plan"] == true ||
|
|
1040
1040
|
content_off_by_simulation_plan?(trace_id:, trace_function_key:, span_name:, instrumentation:) ||
|
|
1041
1041
|
(!parent_span_id.nil? && simulation_plan_unavailable?(instrumentation:)))
|
|
1042
|
+
snapshot = nil
|
|
1042
1043
|
if content_off
|
|
1043
1044
|
dropped = []
|
|
1044
1045
|
span_data = {"name" => span_name, "type" => span_type, "function_name" => function_name}
|
|
@@ -1049,10 +1050,6 @@ module Bitfab
|
|
|
1049
1050
|
human_output, output_dropped = Serialize.serialize_value_with_report(result)
|
|
1050
1051
|
dropped = input_dropped + output_dropped
|
|
1051
1052
|
|
|
1052
|
-
raw_input = (args.length == 1 && kwargs.empty?) ? args[0] : {args:, kwargs:}
|
|
1053
|
-
marshalled_input = Serialize.marshal_value(raw_input)
|
|
1054
|
-
marshalled_output = Serialize.marshal_value(result)
|
|
1055
|
-
|
|
1056
1053
|
span_data = {
|
|
1057
1054
|
"name" => span_name,
|
|
1058
1055
|
"type" => span_type,
|
|
@@ -1061,8 +1058,13 @@ module Bitfab
|
|
|
1061
1058
|
"function_name" => function_name
|
|
1062
1059
|
}
|
|
1063
1060
|
span_data.merge!(extra_span_data)
|
|
1064
|
-
|
|
1065
|
-
|
|
1061
|
+
snapshot = SpanMeta::Snapshot.capture(args, kwargs, result)
|
|
1062
|
+
unless snapshot
|
|
1063
|
+
input_meta = SpanMeta.build_input_meta(args, kwargs, human_inputs)
|
|
1064
|
+
output_meta = SpanMeta.build_output_meta(result, human_output)
|
|
1065
|
+
span_data["input_meta"] = input_meta if input_meta
|
|
1066
|
+
span_data["output_meta"] = output_meta if output_meta
|
|
1067
|
+
end
|
|
1066
1068
|
end
|
|
1067
1069
|
if error
|
|
1068
1070
|
span_data["error"] = error
|
|
@@ -1120,7 +1122,7 @@ module Bitfab
|
|
|
1120
1122
|
}]
|
|
1121
1123
|
end
|
|
1122
1124
|
|
|
1123
|
-
@http_client.send_external_span(payload, declared_node)
|
|
1125
|
+
@http_client.send_external_span(payload, declared_node, snapshot)
|
|
1124
1126
|
end
|
|
1125
1127
|
|
|
1126
1128
|
# Advance the per-(key, name) call counter when this invocation is a
|
data/lib/bitfab/experiments.rb
CHANGED
|
@@ -45,6 +45,15 @@ module Bitfab
|
|
|
45
45
|
@http_client.get("#{PATH}/rollups?#{query}")["rollups"]
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Reads one run's written summary. "status" is "pending" while the run has not finished or its verdicts are
|
|
49
|
+
# still arriving; the summary is written once every verdict is in. "unavailable" means the run ended failed or
|
|
50
|
+
# interrupted and no summary will be written. "ready" carries the summary, compared against the first finished
|
|
51
|
+
# run in the same experiment group for the same traced function, or against the run's original traces when
|
|
52
|
+
# there is none.
|
|
53
|
+
def get_summary(id)
|
|
54
|
+
@http_client.get("#{PATH}/#{encode(id)}/summary")
|
|
55
|
+
end
|
|
56
|
+
|
|
48
57
|
private
|
|
49
58
|
|
|
50
59
|
def iso8601(value)
|
data/lib/bitfab/http_client.rb
CHANGED
|
@@ -193,7 +193,7 @@ module Bitfab
|
|
|
193
193
|
end
|
|
194
194
|
|
|
195
195
|
# Queue an external span on this client's trace transport (fire-and-forget).
|
|
196
|
-
def send_external_span(payload, declared_node = false)
|
|
196
|
+
def send_external_span(payload, declared_node = false, snapshot = nil)
|
|
197
197
|
if (data = payload.dig("rawSpan", "span_data")).is_a?(Hash) && !data.key?("runtime")
|
|
198
198
|
payload = payload.merge("rawSpan" => payload["rawSpan"].merge("span_data" => data.merge("runtime" => ThreadPropagation.runtime)))
|
|
199
199
|
end
|
|
@@ -203,11 +203,16 @@ module Bitfab
|
|
|
203
203
|
submit = ->(record) {
|
|
204
204
|
ref = carrier_ref(record)
|
|
205
205
|
@trace_completion.record(ref.trace_id, ref.span_id) if ref&.span_id
|
|
206
|
-
trace_transport
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
transport = trace_transport
|
|
207
|
+
carrier = record.merge("sdkVersion" => VERSION)
|
|
208
|
+
meta = recorded_meta("external_span", record, ref)
|
|
209
|
+
if snapshot.nil? || !snapshot.content?(carrier)
|
|
210
|
+
transport&.submit("external_span", carrier, meta)
|
|
211
|
+
elsif transport.respond_to?(:submit_with_snapshot)
|
|
212
|
+
transport.submit_with_snapshot("external_span", carrier, meta, snapshot)
|
|
213
|
+
else
|
|
214
|
+
transport&.submit("external_span", snapshot.apply(carrier), meta)
|
|
215
|
+
end
|
|
211
216
|
}
|
|
212
217
|
discard = ->(record) {
|
|
213
218
|
ref = carrier_ref(record)
|
data/lib/bitfab/otel.rb
CHANGED
|
@@ -492,6 +492,94 @@ module Bitfab
|
|
|
492
492
|
# Owns one private TracerProvider + BatchSpanProcessor per client. The
|
|
493
493
|
# provider is never installed globally, so the SDK cannot disturb an
|
|
494
494
|
# application's own OpenTelemetry setup.
|
|
495
|
+
class SpanWorker
|
|
496
|
+
MAX_PENDING = 256
|
|
497
|
+
MAX_PENDING_BYTES = 64 * 1024 * 1024
|
|
498
|
+
|
|
499
|
+
def initialize
|
|
500
|
+
reset
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def enqueue(bytes = 0, &job)
|
|
504
|
+
reset_after_fork
|
|
505
|
+
queued = @mutex.synchronize do
|
|
506
|
+
next false if @queue.closed? || @pending >= MAX_PENDING
|
|
507
|
+
next false if @pending.positive? && @pending_bytes + bytes > MAX_PENDING_BYTES
|
|
508
|
+
|
|
509
|
+
@pending += 1
|
|
510
|
+
@pending_bytes += bytes
|
|
511
|
+
@queue << [job, bytes]
|
|
512
|
+
@thread = Thread.new { work } unless @thread&.alive?
|
|
513
|
+
true
|
|
514
|
+
end
|
|
515
|
+
run(job) unless queued
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def after_pending(&job)
|
|
519
|
+
reset_after_fork
|
|
520
|
+
return run(job) if @mutex.synchronize { @pending.zero? }
|
|
521
|
+
|
|
522
|
+
enqueue(&job)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def drain(timeout)
|
|
526
|
+
reset_after_fork
|
|
527
|
+
deadline = Otel.monotonic_now + [timeout, 0].max
|
|
528
|
+
@mutex.synchronize do
|
|
529
|
+
while @pending.positive?
|
|
530
|
+
remaining = deadline - Otel.monotonic_now
|
|
531
|
+
return false unless remaining.positive?
|
|
532
|
+
|
|
533
|
+
@idle.wait(@mutex, remaining)
|
|
534
|
+
end
|
|
535
|
+
true
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def stop
|
|
540
|
+
@queue.close
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
private
|
|
544
|
+
|
|
545
|
+
def reset
|
|
546
|
+
@owner_pid = Process.pid
|
|
547
|
+
@mutex = Mutex.new
|
|
548
|
+
@idle = ConditionVariable.new
|
|
549
|
+
@queue = Thread::Queue.new
|
|
550
|
+
@pending = 0
|
|
551
|
+
@pending_bytes = 0
|
|
552
|
+
@thread = nil
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def reset_after_fork
|
|
556
|
+
reset unless @owner_pid == Process.pid
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def work
|
|
560
|
+
while (entry = @queue.pop)
|
|
561
|
+
job, bytes = entry
|
|
562
|
+
run(job)
|
|
563
|
+
@mutex.synchronize do
|
|
564
|
+
@pending -= 1
|
|
565
|
+
@pending_bytes -= bytes
|
|
566
|
+
@idle.broadcast if @pending.zero?
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def run(job)
|
|
572
|
+
job.call
|
|
573
|
+
rescue => e
|
|
574
|
+
Bitfab.warn_once(
|
|
575
|
+
"span-worker-failed",
|
|
576
|
+
"failed to finish a span in the background (further occurrences " \
|
|
577
|
+
"suppressed): #{e.message}"
|
|
578
|
+
)
|
|
579
|
+
nil
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
495
583
|
class BatchTransport
|
|
496
584
|
attr_reader :owner_pid
|
|
497
585
|
|
|
@@ -510,57 +598,40 @@ module Bitfab
|
|
|
510
598
|
@owner_pid = Process.pid
|
|
511
599
|
@state_mutex = Mutex.new
|
|
512
600
|
@flush_mutex = Mutex.new
|
|
601
|
+
@span_worker = SpanWorker.new
|
|
513
602
|
@closed = false
|
|
514
603
|
create_pipeline
|
|
515
604
|
Otel.register_transport(self)
|
|
516
605
|
end
|
|
517
606
|
|
|
518
607
|
def submit(operation, payload, meta = nil)
|
|
519
|
-
meta
|
|
608
|
+
meta = stamped(meta)
|
|
520
609
|
# Encoding is the expensive part of a submit and needs no mutual
|
|
521
610
|
# exclusion, so it stays outside the lock.
|
|
522
|
-
|
|
523
|
-
encoded_payload
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
)
|
|
527
|
-
|
|
528
|
-
started_at = meta.started_at || Time.now
|
|
529
|
-
ended_at = meta.ended_at || Time.now
|
|
530
|
-
errored = meta.errored
|
|
531
|
-
|
|
532
|
-
# Pipeline construction and the closed check are serialized: concurrent
|
|
533
|
-
# first submits in a forked child would otherwise each build a pipeline,
|
|
534
|
-
# orphaning one batch worker along with whatever it had queued.
|
|
535
|
-
@state_mutex.synchronize do
|
|
536
|
-
return warn_closed if @closed
|
|
537
|
-
|
|
538
|
-
ensure_process
|
|
539
|
-
tracer = @tracer
|
|
540
|
-
return warn_closed if tracer.nil?
|
|
611
|
+
encoded_payload = encode_payload(payload, meta)
|
|
612
|
+
@span_worker.after_pending { emit(operation, encoded_payload, meta) }
|
|
613
|
+
nil
|
|
614
|
+
rescue => e
|
|
615
|
+
warn_submit_failed(e)
|
|
616
|
+
end
|
|
541
617
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
start_timestamp: started_at
|
|
549
|
-
)
|
|
550
|
-
span.status = OpenTelemetry::Trace::Status.error if errored
|
|
551
|
-
span.finish(end_timestamp: ended_at)
|
|
618
|
+
def submit_with_snapshot(operation, payload, meta, snapshot)
|
|
619
|
+
meta = stamped(meta)
|
|
620
|
+
encoded_payload = encode_payload(payload, meta)
|
|
621
|
+
@span_worker.enqueue(encoded_payload.bytesize + snapshot.bytesize) do
|
|
622
|
+
completed = complete_with_snapshot(encoded_payload, snapshot)
|
|
623
|
+
emit(operation, completed ? encode_payload(completed, meta) : encoded_payload, meta)
|
|
552
624
|
end
|
|
553
625
|
nil
|
|
554
626
|
rescue => e
|
|
555
|
-
|
|
556
|
-
"otel-submit-failed",
|
|
557
|
-
"failed to queue an OpenTelemetry span (further occurrences " \
|
|
558
|
-
"suppressed): #{e.message}"
|
|
559
|
-
)
|
|
560
|
-
nil
|
|
627
|
+
warn_submit_failed(e)
|
|
561
628
|
end
|
|
562
629
|
|
|
563
630
|
def flush(timeout = 30.0)
|
|
631
|
+
deadline = Otel.monotonic_now + [timeout, 0].max
|
|
632
|
+
return false unless @span_worker.drain(timeout)
|
|
633
|
+
|
|
634
|
+
timeout = [deadline - Otel.monotonic_now, 0].max
|
|
564
635
|
processor, delivery_tracker = @state_mutex.synchronize do
|
|
565
636
|
ensure_process
|
|
566
637
|
[@processor, @delivery_tracker]
|
|
@@ -591,8 +662,10 @@ module Bitfab
|
|
|
591
662
|
|
|
592
663
|
def shutdown(timeout = 30.0)
|
|
593
664
|
deadline = Otel.monotonic_now + [timeout, 0].max
|
|
665
|
+
drained = @span_worker.drain(timeout)
|
|
594
666
|
@state_mutex.synchronize { @closed = true }
|
|
595
|
-
|
|
667
|
+
@span_worker.stop
|
|
668
|
+
flushed = flush([deadline - Otel.monotonic_now, 0].max) && drained
|
|
596
669
|
|
|
597
670
|
processor = @state_mutex.synchronize do
|
|
598
671
|
current = @processor
|
|
@@ -611,6 +684,68 @@ module Bitfab
|
|
|
611
684
|
|
|
612
685
|
private
|
|
613
686
|
|
|
687
|
+
def stamped(meta)
|
|
688
|
+
meta = (meta || CarrierMeta.new).dup
|
|
689
|
+
now = Time.now
|
|
690
|
+
meta.started_at ||= now
|
|
691
|
+
meta.ended_at ||= now
|
|
692
|
+
meta
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def encode_payload(payload, meta)
|
|
696
|
+
encoded = Serialize.safe_generate(
|
|
697
|
+
payload,
|
|
698
|
+
max_carrier_bytes: PayloadBudget::MAX_COMPRESSIBLE_SPAN_CARRIER_BYTES
|
|
699
|
+
)
|
|
700
|
+
encoded.instance_variable_set(CARRIER_REF_IVAR, meta.ref) if meta.ref
|
|
701
|
+
encoded
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
def complete_with_snapshot(encoded_payload, snapshot)
|
|
705
|
+
snapshot.apply(JSON.parse(encoded_payload))
|
|
706
|
+
rescue StandardError, SystemStackError => e
|
|
707
|
+
Bitfab.warn_once(
|
|
708
|
+
"span-meta-snapshot-failed",
|
|
709
|
+
"could not record replay type details for a span; it will replay " \
|
|
710
|
+
"from its readable value (further occurrences suppressed): #{e.message}"
|
|
711
|
+
)
|
|
712
|
+
nil
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Pipeline construction and the closed check are serialized: concurrent
|
|
716
|
+
# first submits in a forked child would otherwise each build a pipeline,
|
|
717
|
+
# orphaning one batch worker along with whatever it had queued.
|
|
718
|
+
def emit(operation, encoded_payload, meta)
|
|
719
|
+
@state_mutex.synchronize do
|
|
720
|
+
return warn_closed if @closed
|
|
721
|
+
|
|
722
|
+
ensure_process
|
|
723
|
+
tracer = @tracer
|
|
724
|
+
return warn_closed if tracer.nil?
|
|
725
|
+
|
|
726
|
+
span = tracer.start_root_span(
|
|
727
|
+
meta.name || "bitfab.#{operation}",
|
|
728
|
+
attributes: {
|
|
729
|
+
OPERATION_ATTRIBUTE => operation,
|
|
730
|
+
PAYLOAD_ATTRIBUTE => encoded_payload
|
|
731
|
+
},
|
|
732
|
+
start_timestamp: meta.started_at
|
|
733
|
+
)
|
|
734
|
+
span.status = OpenTelemetry::Trace::Status.error if meta.errored
|
|
735
|
+
span.finish(end_timestamp: meta.ended_at)
|
|
736
|
+
end
|
|
737
|
+
nil
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def warn_submit_failed(error)
|
|
741
|
+
Bitfab.warn_once(
|
|
742
|
+
"otel-submit-failed",
|
|
743
|
+
"failed to queue an OpenTelemetry span (further occurrences " \
|
|
744
|
+
"suppressed): #{error.message}"
|
|
745
|
+
)
|
|
746
|
+
nil
|
|
747
|
+
end
|
|
748
|
+
|
|
614
749
|
def finish_shutdown(succeeded)
|
|
615
750
|
Otel.unregister_transport(self)
|
|
616
751
|
succeeded
|
data/lib/bitfab/replay.rb
CHANGED
|
@@ -1278,8 +1278,6 @@ module Bitfab
|
|
|
1278
1278
|
|
|
1279
1279
|
span = http_client.get_external_span(external_span_id, replay_view: true)
|
|
1280
1280
|
span_data = (span["rawData"] || {})["span_data"] || {}
|
|
1281
|
-
# Prefer the Ruby Marshal payload (output_serialized) written by this
|
|
1282
|
-
# SDK; fall back to another SDK's output_meta, then the raw JSON output.
|
|
1283
1281
|
meta = span_data["output_serialized"]
|
|
1284
1282
|
meta = span_data["output_meta"] if meta.nil? || meta == ""
|
|
1285
1283
|
cache[external_span_id] = Serialize.deserialize_output(span_data["output"], meta)
|
|
@@ -1294,6 +1292,7 @@ module Bitfab
|
|
|
1294
1292
|
{
|
|
1295
1293
|
"input" => span_data["input"],
|
|
1296
1294
|
"output" => span_data["output"],
|
|
1295
|
+
"inputMeta" => span_data["input_meta"],
|
|
1297
1296
|
"inputSerialized" => span_data["input_serialized"],
|
|
1298
1297
|
"outputSerialized" => span_data["output_serialized"]
|
|
1299
1298
|
}
|
data/lib/bitfab/seed.rb
CHANGED
|
@@ -99,7 +99,7 @@ module Bitfab
|
|
|
99
99
|
unless source.fetch("traceFunctionKey") == trace_function_key
|
|
100
100
|
raise ArgumentError, "Trace #{trace_id} belongs to '#{source["traceFunctionKey"]}', not '#{trace_function_key}'."
|
|
101
101
|
end
|
|
102
|
-
args, kwargs = Serialize.deserialize_inputs({"input" => source["input"], "
|
|
102
|
+
args, kwargs = Serialize.deserialize_inputs({"input" => source["input"], "inputSerialized" => source["inputSerialized"], "inputMeta" => source["inputMeta"]})
|
|
103
103
|
run_trace_id = seed_trace(trace_function_key, callable, args:, kwargs:,
|
|
104
104
|
metadata: source["metadata"], session_id: source["sessionId"], name: source["name"])
|
|
105
105
|
adopted = @http_client.request(endpoint, {runTraceId: run_trace_id})
|
data/lib/bitfab/serialize.rb
CHANGED
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "time"
|
|
6
6
|
|
|
7
7
|
require_relative "payload_budget"
|
|
8
|
+
require_relative "span_meta"
|
|
8
9
|
|
|
9
10
|
module Bitfab
|
|
10
11
|
module Serialize
|
|
@@ -196,35 +197,19 @@ module Bitfab
|
|
|
196
197
|
Marshal.load(Base64.strict_decode64(encoded)) # rubocop:disable Security/MarshalLoad
|
|
197
198
|
end
|
|
198
199
|
|
|
199
|
-
# Deserialize a recorded span output. Prefers the Ruby-side Marshal+Base64
|
|
200
|
-
# `meta` when present (type-preserving); falls back to the raw JSON output
|
|
201
|
-
# when meta is absent or a shape Ruby cannot reconstruct (e.g. the
|
|
202
|
-
# superjson/jsonpickle meta another SDK's span may carry). Shared by the
|
|
203
|
-
# inline mock-tree path and the lazy per-span fetch so both deserialize
|
|
204
|
-
# identically.
|
|
205
|
-
#
|
|
206
|
-
# @param raw_output [Object] the human-readable JSON output
|
|
207
|
-
# @param meta [String, nil] the Marshal+Base64 metadata, if any
|
|
208
|
-
# @return [Object] the reconstructed output, or raw_output on fallback
|
|
209
200
|
def deserialize_output(raw_output, meta)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
end
|
|
216
|
-
end
|
|
201
|
+
return SpanMeta.restore_output(raw_output, meta) if meta.is_a?(Hash)
|
|
202
|
+
return raw_output unless meta.is_a?(String) && !meta.empty?
|
|
203
|
+
|
|
204
|
+
unmarshal_value(meta)
|
|
205
|
+
rescue
|
|
217
206
|
raw_output
|
|
218
207
|
end
|
|
219
208
|
|
|
220
|
-
# Deserialize replay inputs from a span's data into [args, kwargs].
|
|
221
|
-
#
|
|
222
|
-
# Prefers Marshal-serialized `inputSerialized` for type preservation,
|
|
223
|
-
# falls back to the raw `input` field.
|
|
224
|
-
#
|
|
225
|
-
# @param item [Hash] with "inputSerialized" and/or "input" keys
|
|
226
|
-
# @return [Array(Array, Hash)] positional args and keyword args
|
|
227
209
|
def deserialize_inputs(item)
|
|
210
|
+
restored = SpanMeta.restore_inputs(item["input"], item["inputMeta"])
|
|
211
|
+
return restored if restored
|
|
212
|
+
|
|
228
213
|
input_serialized = item["inputSerialized"]
|
|
229
214
|
raw_input = item["input"]
|
|
230
215
|
|
|
@@ -236,7 +221,7 @@ module Bitfab
|
|
|
236
221
|
end
|
|
237
222
|
return deserialized.nil? ? [[], {}] : [[deserialized], {}]
|
|
238
223
|
rescue
|
|
239
|
-
|
|
224
|
+
nil
|
|
240
225
|
end
|
|
241
226
|
end
|
|
242
227
|
|
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module Bitfab
|
|
8
|
+
module SpanMeta
|
|
9
|
+
MAX_NODES = 1_000_000
|
|
10
|
+
PLAIN_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII].freeze
|
|
11
|
+
|
|
12
|
+
class NeedsCopy < StandardError; end
|
|
13
|
+
|
|
14
|
+
class GiveUp < StandardError; end
|
|
15
|
+
|
|
16
|
+
class Cycle < StandardError
|
|
17
|
+
attr_reader :target
|
|
18
|
+
|
|
19
|
+
def initialize(target)
|
|
20
|
+
@target = target
|
|
21
|
+
super("value refers back to #{target.inspect}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
SpanType = Struct.new(:name, :matches, :describe, :rebuild)
|
|
26
|
+
|
|
27
|
+
PathTrie = Struct.new(:children, :annotation, :copy, :same, :store, :symbol)
|
|
28
|
+
|
|
29
|
+
extend self
|
|
30
|
+
|
|
31
|
+
def build_input_meta(args, kwargs, readable_input)
|
|
32
|
+
meta = {}
|
|
33
|
+
meta["kwarg_names"] = kwargs.keys.map { |name| Serialize.safe_to_s(name) } unless kwargs.empty?
|
|
34
|
+
root = kwargs.empty? ? args.dup : args + [kwargs]
|
|
35
|
+
symbol_paths = kwargs.empty? ? [] : [[args.length.to_s]]
|
|
36
|
+
meta.merge!(value_meta(root, readable_input, symbol_paths))
|
|
37
|
+
meta.empty? ? nil : meta
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def build_output_meta(value, readable_output)
|
|
41
|
+
meta = value_meta(value, readable_output, [])
|
|
42
|
+
meta.empty? ? nil : meta
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def restore_inputs(raw_input, input_meta)
|
|
46
|
+
return nil unless input_meta.is_a?(Hash) && raw_input.is_a?(Array)
|
|
47
|
+
|
|
48
|
+
kwargs_path = input_meta["kwarg_names"]&.any? ? [(raw_input.length - 1).to_s] : nil
|
|
49
|
+
root = restore(raw_input, input_meta, kwargs_path ? [kwargs_path] : [])
|
|
50
|
+
return nil unless root.is_a?(Array)
|
|
51
|
+
|
|
52
|
+
args = root.dup
|
|
53
|
+
return [args, {}] unless kwargs_path
|
|
54
|
+
|
|
55
|
+
kwargs = args.pop
|
|
56
|
+
kwargs.is_a?(Hash) ? [args, kwargs] : nil
|
|
57
|
+
rescue
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def restore_output(raw_output, output_meta)
|
|
62
|
+
return raw_output unless output_meta.is_a?(Hash)
|
|
63
|
+
|
|
64
|
+
restore(raw_output, output_meta, [])
|
|
65
|
+
rescue
|
|
66
|
+
raw_output
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def value_meta(value, readable, symbol_paths)
|
|
70
|
+
recorder = Recorder.new(symbol_paths)
|
|
71
|
+
recorder.walk(value, readable, [])
|
|
72
|
+
meta = recorder.meta
|
|
73
|
+
raise GiveUp if meta.key?("v") && !restores_exactly?(value, readable, meta, recorder.lossy, symbol_paths)
|
|
74
|
+
|
|
75
|
+
meta
|
|
76
|
+
rescue StandardError, SystemStackError
|
|
77
|
+
copy = Serialize.marshal_value(value)
|
|
78
|
+
copy ? {"copy" => copy} : {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def restores_exactly?(value, readable, meta, lossy, symbol_paths)
|
|
82
|
+
restored = restore(JSON.parse(JSON.generate(readable)), JSON.parse(JSON.generate(meta)), symbol_paths)
|
|
83
|
+
Exact.new(lossy).same?(value, restored, [])
|
|
84
|
+
rescue StandardError, SystemStackError
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def tracks_identity?(value)
|
|
89
|
+
!(value.nil? || true.equal?(value) || false.equal?(value) || value.is_a?(Numeric) ||
|
|
90
|
+
value.is_a?(Symbol) || value.is_a?(Module) || value.frozen? || value.is_a?(String) || value.is_a?(Time) || value.is_a?(Date))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def require!(condition)
|
|
94
|
+
raise NeedsCopy unless condition
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def class_name_for(klass)
|
|
98
|
+
name = klass.name
|
|
99
|
+
raise NeedsCopy if name.nil? || name.empty?
|
|
100
|
+
|
|
101
|
+
name
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def symbol_keys_note(hash, symbol_count, symbol_default)
|
|
105
|
+
kind = if symbol_count.zero?
|
|
106
|
+
"string"
|
|
107
|
+
elsif symbol_count == hash.size
|
|
108
|
+
"symbol"
|
|
109
|
+
else
|
|
110
|
+
hash.keys.select { |key| key.is_a?(Symbol) }.map(&:name)
|
|
111
|
+
end
|
|
112
|
+
default = symbol_default ? "symbol" : "string"
|
|
113
|
+
(kind == default) ? nil : kind
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def plain_string?(value, readable)
|
|
117
|
+
return false unless readable.is_a?(String) && readable == value && value.instance_variables.empty?
|
|
118
|
+
|
|
119
|
+
case value.encoding
|
|
120
|
+
when Encoding::UTF_8 then value.valid_encoding?
|
|
121
|
+
when Encoding::US_ASCII then value.ascii_only?
|
|
122
|
+
else false
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class Snapshot
|
|
127
|
+
def self.capture(args, kwargs, result)
|
|
128
|
+
new(Marshal.dump([args, kwargs, result]))
|
|
129
|
+
rescue StandardError, SystemStackError
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def initialize(bytes)
|
|
134
|
+
@bytes = bytes
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def bytesize
|
|
138
|
+
@bytes.bytesize
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def content?(payload)
|
|
142
|
+
data = payload.dig("rawSpan", "span_data")
|
|
143
|
+
data.is_a?(Hash) && !data["content_off_by_simulation_plan"] && (data.key?("input") || data.key?("output"))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def apply(payload)
|
|
147
|
+
return payload unless content?(payload)
|
|
148
|
+
|
|
149
|
+
data = payload["rawSpan"]["span_data"]
|
|
150
|
+
args, kwargs, result = Marshal.load(@bytes) # rubocop:disable Security/MarshalLoad
|
|
151
|
+
input_meta = SpanMeta.build_input_meta(args, kwargs, data["input"]) if data.key?("input")
|
|
152
|
+
output_meta = SpanMeta.build_output_meta(result, data["output"]) if data.key?("output")
|
|
153
|
+
data["input_meta"] = input_meta if input_meta
|
|
154
|
+
data["output_meta"] = output_meta if output_meta
|
|
155
|
+
payload
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
class Node
|
|
160
|
+
def initialize(recorder, path)
|
|
161
|
+
@recorder = recorder
|
|
162
|
+
@path = path
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def walk(child, readable, key)
|
|
166
|
+
@recorder.walk(child, readable, @path + [key.to_s])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def walk_items(items, readable)
|
|
170
|
+
SpanMeta.require!(readable.is_a?(Array) && readable.length == items.length)
|
|
171
|
+
items.each_with_index { |item, index| walk(item, readable[index], index) }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def symbol_keys_default?
|
|
175
|
+
@recorder.symbol_path?(@path)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
class Recorder
|
|
180
|
+
attr_reader :lossy
|
|
181
|
+
|
|
182
|
+
def initialize(symbol_paths)
|
|
183
|
+
@symbol_paths = symbol_paths
|
|
184
|
+
@values = {}
|
|
185
|
+
@same = []
|
|
186
|
+
@copies = {}
|
|
187
|
+
@seen = {}.compare_by_identity
|
|
188
|
+
@dropped = []
|
|
189
|
+
@lossy = []
|
|
190
|
+
@nodes = 0
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def symbol_path?(path)
|
|
194
|
+
@symbol_paths.include?(path)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def meta
|
|
198
|
+
meta = {}
|
|
199
|
+
meta["values"] = SpanMeta.values_tree(@values) unless @values.empty?
|
|
200
|
+
meta["referentialEqualities"] = SpanMeta.equalities_record(@same) unless @same.empty?
|
|
201
|
+
meta["v"] = 1 unless meta.empty?
|
|
202
|
+
if @copies.values.sum(&:bytesize) > Serialize::MAX_SERIALIZED_BYTES
|
|
203
|
+
@lossy.concat(@copies.keys)
|
|
204
|
+
elsif @copies.key?([])
|
|
205
|
+
meta["copy"] = @copies[[]]
|
|
206
|
+
elsif !@copies.empty?
|
|
207
|
+
meta["copies"] = @copies.transform_keys { |path| SpanMeta.stringify_path(path) }
|
|
208
|
+
end
|
|
209
|
+
meta
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def walk(value, readable, path)
|
|
213
|
+
@nodes += 1
|
|
214
|
+
raise GiveUp if @nodes > MAX_NODES
|
|
215
|
+
|
|
216
|
+
values_mark = @values.size
|
|
217
|
+
same_mark = @same.size
|
|
218
|
+
copies_mark = @copies.size
|
|
219
|
+
lossy_mark = @lossy.size
|
|
220
|
+
if SpanMeta.tracks_identity?(value)
|
|
221
|
+
first = @seen[value]
|
|
222
|
+
if first && !dropped?(first)
|
|
223
|
+
raise Cycle.new(first) if path.first(first.length) == first
|
|
224
|
+
|
|
225
|
+
@same << [path, first]
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
@seen[value] = path
|
|
229
|
+
end
|
|
230
|
+
annotate(value, readable, path)
|
|
231
|
+
rescue GiveUp
|
|
232
|
+
raise
|
|
233
|
+
rescue Cycle => cycle
|
|
234
|
+
raise unless cycle.target == path
|
|
235
|
+
|
|
236
|
+
copy_here(value, path, values_mark, same_mark, copies_mark, lossy_mark)
|
|
237
|
+
rescue StandardError, SystemStackError
|
|
238
|
+
copy_here(value, path, values_mark, same_mark, copies_mark, lossy_mark)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private
|
|
242
|
+
|
|
243
|
+
def copy_here(value, path, values_mark, same_mark, copies_mark, lossy_mark)
|
|
244
|
+
drop_after(values_mark, same_mark, copies_mark, lossy_mark)
|
|
245
|
+
@dropped << path
|
|
246
|
+
copy = Serialize.marshal_value(value)
|
|
247
|
+
copy ? @copies[path] = copy : @lossy << path
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def drop_after(values_size, same_size, copies_size, lossy_size)
|
|
251
|
+
@values.keys.drop(values_size).each { |key| @values.delete(key) } if @values.size > values_size
|
|
252
|
+
@same.slice!(same_size..) if @same.size > same_size
|
|
253
|
+
@copies.keys.drop(copies_size).each { |key| @copies.delete(key) } if @copies.size > copies_size
|
|
254
|
+
@lossy.slice!(lossy_size..) if @lossy.size > lossy_size
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def dropped?(path)
|
|
258
|
+
@dropped.any? { |prefix| path.first(prefix.length) == prefix }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def annotate(value, readable, path)
|
|
262
|
+
case value
|
|
263
|
+
when nil, true, false
|
|
264
|
+
SpanMeta.require!(readable.equal?(value))
|
|
265
|
+
when Integer
|
|
266
|
+
SpanMeta.require!(readable.is_a?(Integer) && readable == value)
|
|
267
|
+
when Float
|
|
268
|
+
SpanMeta.require!(value.finite? && readable.is_a?(Float) && readable == value)
|
|
269
|
+
else
|
|
270
|
+
return if value.instance_of?(String) && SpanMeta.plain_string?(value, readable)
|
|
271
|
+
return Node.new(self, path).walk_items(value, readable) if value.instance_of?(Array)
|
|
272
|
+
|
|
273
|
+
span_type = value.instance_of?(Hash) ? SPAN_TYPES_BY_NAME["rb:hash"] : SPAN_TYPES.find { |entry| entry.matches.call(value) }
|
|
274
|
+
raise NeedsCopy unless span_type
|
|
275
|
+
|
|
276
|
+
params = span_type.describe.call(value, readable, Node.new(self, path))
|
|
277
|
+
@values[path] = ["custom", span_type.name] + (params.empty? ? [] : [params]) if params
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def describe_symbol(value, readable, _node)
|
|
283
|
+
require!(readable == value.to_s)
|
|
284
|
+
{}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def describe_string(value, readable, _node)
|
|
288
|
+
require!(readable.is_a?(String) && readable == value && value.dup.force_encoding(Encoding::UTF_8).valid_encoding?)
|
|
289
|
+
require!(value.instance_variables.empty?)
|
|
290
|
+
params = {}
|
|
291
|
+
params["class"] = class_name_for(value.class) unless value.instance_of?(String)
|
|
292
|
+
params["encoding"] = value.encoding.name unless PLAIN_ENCODINGS.include?(value.encoding)
|
|
293
|
+
params.empty? ? nil : params
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def describe_array(value, readable, node)
|
|
297
|
+
node.walk_items(value, readable)
|
|
298
|
+
value.instance_of?(Array) ? nil : {"class" => class_name_for(value.class)}
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def describe_hash(value, readable, node)
|
|
302
|
+
require!(value.default.nil? && value.default_proc.nil? && !value.compare_by_identity?)
|
|
303
|
+
require!(readable.is_a?(Hash) && readable.size == value.size)
|
|
304
|
+
readable_keys = readable.keys
|
|
305
|
+
symbol_count = 0
|
|
306
|
+
value.each_with_index do |(key, item), index|
|
|
307
|
+
name = case key
|
|
308
|
+
when String then key
|
|
309
|
+
when Symbol
|
|
310
|
+
symbol_count += 1
|
|
311
|
+
key.name
|
|
312
|
+
else raise NeedsCopy
|
|
313
|
+
end
|
|
314
|
+
require!(readable_keys[index] == name)
|
|
315
|
+
node.walk(item, readable[name], name)
|
|
316
|
+
end
|
|
317
|
+
params = {}
|
|
318
|
+
params["class"] = class_name_for(value.class) unless value.instance_of?(Hash)
|
|
319
|
+
keys = symbol_keys_note(value, symbol_count, node.symbol_keys_default?)
|
|
320
|
+
params["keys"] = keys if keys
|
|
321
|
+
params.empty? ? nil : params
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def describe_set(value, readable, node)
|
|
325
|
+
require!(value.instance_of?(Set))
|
|
326
|
+
node.walk_items(value.to_a, readable)
|
|
327
|
+
{}
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def describe_time(value, readable, _node)
|
|
331
|
+
require!(value.instance_of?(Time) && readable == value.iso8601(3))
|
|
332
|
+
{"nsec" => value.nsec}
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def describe_date(value, readable, _node)
|
|
336
|
+
require!(value.instance_of?(Date) && readable == value.to_s)
|
|
337
|
+
{}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def describe_bigdecimal(value, readable, _node)
|
|
341
|
+
require!(readable == value.to_s)
|
|
342
|
+
{}
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def describe_struct(value, readable, node)
|
|
346
|
+
members = value.members
|
|
347
|
+
require!(readable.is_a?(Hash) && readable.keys == members.map(&:to_s))
|
|
348
|
+
members.each { |member| node.walk(value[member], readable[member.to_s], member) }
|
|
349
|
+
{"class" => class_name_for(value.class)}
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def rebuild_array(built, params)
|
|
353
|
+
Array.instance_method(:replace).bind_call(resolve(params["class"]).allocate, built)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def rebuild_string(built, params)
|
|
357
|
+
text = params["class"] ? String.instance_method(:replace).bind_call(resolve(params["class"]).allocate, built) : built.dup
|
|
358
|
+
params["encoding"] ? text.force_encoding(params["encoding"]) : text
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def rebuild_hash(built, params)
|
|
362
|
+
keys = params["keys"]
|
|
363
|
+
hash = case keys
|
|
364
|
+
when "symbol" then built.transform_keys(&:to_sym)
|
|
365
|
+
when Array then built.transform_keys { |key| keys.include?(key) ? key.to_sym : key }
|
|
366
|
+
else built
|
|
367
|
+
end
|
|
368
|
+
return hash unless params["class"]
|
|
369
|
+
|
|
370
|
+
Hash.instance_method(:replace).bind_call(resolve(params["class"]).allocate, hash)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def rebuild_time(built, params)
|
|
374
|
+
base = begin
|
|
375
|
+
Time.new(built)
|
|
376
|
+
rescue ArgumentError
|
|
377
|
+
Time.iso8601(built)
|
|
378
|
+
end
|
|
379
|
+
zone = built.end_with?("Z") ? "UTC" : base.strftime("%:z")
|
|
380
|
+
Time.at(base.to_i, params["nsec"], :nsec, in: zone)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def rebuild_struct(built, params)
|
|
384
|
+
klass = resolve(params["class"])
|
|
385
|
+
struct = klass.allocate
|
|
386
|
+
klass.members.each { |member| struct[member] = built.fetch(member.to_s) }
|
|
387
|
+
struct
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def resolve(name)
|
|
391
|
+
Object.const_get(name)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def stringify_path(path)
|
|
395
|
+
path.map { |segment| escape_segment(segment) }.join(".")
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def escape_segment(segment)
|
|
399
|
+
return segment unless segment.include?(".") || segment.include?("\\")
|
|
400
|
+
|
|
401
|
+
segment.gsub("\\") { "\\\\" }.gsub(".") { "\\." }
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def parse_path(text)
|
|
405
|
+
return (text.empty? ? [""] : text.split(".", -1)) unless text.include?("\\")
|
|
406
|
+
|
|
407
|
+
segments = []
|
|
408
|
+
segment = +""
|
|
409
|
+
index = 0
|
|
410
|
+
while index < text.length
|
|
411
|
+
char = text[index]
|
|
412
|
+
if char == "\\"
|
|
413
|
+
escaped = text[index + 1]
|
|
414
|
+
raise ArgumentError, "invalid path #{text.inspect}" unless escaped == "\\" || escaped == "."
|
|
415
|
+
|
|
416
|
+
segment << escaped
|
|
417
|
+
index += 2
|
|
418
|
+
next
|
|
419
|
+
end
|
|
420
|
+
if char == "."
|
|
421
|
+
segments << segment
|
|
422
|
+
segment = +""
|
|
423
|
+
else
|
|
424
|
+
segment << char
|
|
425
|
+
end
|
|
426
|
+
index += 1
|
|
427
|
+
end
|
|
428
|
+
segments << segment
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def values_tree(annotations)
|
|
432
|
+
root = {}
|
|
433
|
+
annotations.each_key do |path|
|
|
434
|
+
node = path.reduce(root) { |parent, segment| parent[segment] ||= {} }
|
|
435
|
+
node[:path] = path
|
|
436
|
+
end
|
|
437
|
+
tree_for = nil
|
|
438
|
+
collect = lambda do |node, prefix, out|
|
|
439
|
+
node.each do |segment, child|
|
|
440
|
+
next unless segment.is_a?(String)
|
|
441
|
+
|
|
442
|
+
relative = prefix + [segment]
|
|
443
|
+
if child.key?(:path)
|
|
444
|
+
out[stringify_path(relative)] = tree_for.call(child)
|
|
445
|
+
else
|
|
446
|
+
collect.call(child, relative, out)
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
out
|
|
450
|
+
end
|
|
451
|
+
tree_for = lambda do |node|
|
|
452
|
+
kids = collect.call(node, [], {})
|
|
453
|
+
kids.empty? ? [annotations[node[:path]]] : [annotations[node[:path]], kids]
|
|
454
|
+
end
|
|
455
|
+
root.key?(:path) ? tree_for.call(root) : collect.call(root, [], {})
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def flatten_values(tree, origin = [], out = {})
|
|
459
|
+
case tree
|
|
460
|
+
when Hash
|
|
461
|
+
tree.each { |key, child| flatten_values(child, origin + parse_path(key), out) }
|
|
462
|
+
when Array
|
|
463
|
+
unless tree.empty?
|
|
464
|
+
flatten_values(tree[1], origin, out) if tree[1].is_a?(Hash)
|
|
465
|
+
out[origin] = tree[0]
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
out
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def equalities_record(same)
|
|
472
|
+
same.each_with_object({}) do |(path, first), record|
|
|
473
|
+
(record[stringify_path(first)] ||= []) << stringify_path(path)
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def parse_equalities(annotations)
|
|
478
|
+
annotations = annotations[1] if annotations.is_a?(Array)
|
|
479
|
+
return {} unless annotations.is_a?(Hash)
|
|
480
|
+
|
|
481
|
+
annotations.each_with_object({}) do |(first, paths), out|
|
|
482
|
+
next unless paths.is_a?(Array)
|
|
483
|
+
|
|
484
|
+
paths.each { |path| out[parse_path(path)] = parse_path(first) if path.is_a?(String) }
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def restore_trie(meta, symbol_paths)
|
|
489
|
+
root = PathTrie.new({})
|
|
490
|
+
insert = lambda do |path|
|
|
491
|
+
path.reduce(root) { |node, segment| node.children[segment] ||= PathTrie.new({}) }
|
|
492
|
+
end
|
|
493
|
+
flatten_values(meta["values"]).each { |path, annotation| insert.call(path).annotation = annotation }
|
|
494
|
+
parse_equalities(meta["referentialEqualities"]).each do |path, target|
|
|
495
|
+
insert.call(path).same = target
|
|
496
|
+
insert.call(target).store = target
|
|
497
|
+
end
|
|
498
|
+
meta["copies"].each { |path, copy| insert.call(parse_path(path)).copy = [copy] } if meta["copies"].is_a?(Hash)
|
|
499
|
+
symbol_paths.each { |path| insert.call(path).symbol = true }
|
|
500
|
+
root
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def restore(readable, meta, symbol_paths)
|
|
504
|
+
return unmarshal_or_keep(meta["copy"], readable) if meta.key?("copy")
|
|
505
|
+
|
|
506
|
+
trie = restore_trie(meta, symbol_paths)
|
|
507
|
+
return readable if trie.children.empty? && trie.annotation.nil? && !trie.symbol
|
|
508
|
+
|
|
509
|
+
built = {}
|
|
510
|
+
build = lambda do |node, trie_node|
|
|
511
|
+
return node if trie_node.nil?
|
|
512
|
+
|
|
513
|
+
result = if trie_node.copy
|
|
514
|
+
unmarshal_or_keep(trie_node.copy.first, node)
|
|
515
|
+
elsif trie_node.same
|
|
516
|
+
built.fetch(trie_node.same, node)
|
|
517
|
+
else
|
|
518
|
+
children = if trie_node.children.empty?
|
|
519
|
+
node
|
|
520
|
+
else
|
|
521
|
+
case node
|
|
522
|
+
when Array then node.each_with_index.map { |item, index| build.call(item, trie_node.children[index.to_s]) }
|
|
523
|
+
when Hash then node.to_h { |key, item| [key, build.call(item, trie_node.children[key])] }
|
|
524
|
+
else node
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
apply_or_keep(annotation_for(trie_node.annotation, children, trie_node.symbol), children)
|
|
528
|
+
end
|
|
529
|
+
built[trie_node.store] = result if trie_node.store
|
|
530
|
+
result
|
|
531
|
+
end
|
|
532
|
+
build.call(readable, trie)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def annotation_for(annotation, children, symbol_default)
|
|
536
|
+
return annotation unless children.is_a?(Hash)
|
|
537
|
+
return annotation if annotation.nil? && !symbol_default
|
|
538
|
+
|
|
539
|
+
annotation ||= ["custom", "rb:hash"]
|
|
540
|
+
return annotation unless annotation[1] == "rb:hash"
|
|
541
|
+
|
|
542
|
+
params = annotation[2].is_a?(Hash) ? annotation[2] : {}
|
|
543
|
+
return annotation if params.key?("keys")
|
|
544
|
+
|
|
545
|
+
["custom", "rb:hash", params.merge("keys" => symbol_default ? "symbol" : "string")]
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def apply_or_keep(annotation, built)
|
|
549
|
+
return built unless annotation.is_a?(Array) && annotation[0] == "custom"
|
|
550
|
+
|
|
551
|
+
span_type = SPAN_TYPES_BY_NAME[annotation[1]]
|
|
552
|
+
return built unless span_type
|
|
553
|
+
|
|
554
|
+
span_type.rebuild.call(built, annotation[2].is_a?(Hash) ? annotation[2] : {})
|
|
555
|
+
rescue
|
|
556
|
+
built
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def unmarshal_or_keep(encoded, readable)
|
|
560
|
+
return readable unless encoded.is_a?(String) && !encoded.empty?
|
|
561
|
+
|
|
562
|
+
Serialize.unmarshal_value(encoded)
|
|
563
|
+
rescue
|
|
564
|
+
readable
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
SPAN_TYPES = [
|
|
568
|
+
SpanType.new("rb:symbol", ->(value) { value.is_a?(Symbol) }, method(:describe_symbol), ->(built, _) { built.to_sym }),
|
|
569
|
+
SpanType.new("rb:string", ->(value) { value.is_a?(String) }, method(:describe_string), method(:rebuild_string)),
|
|
570
|
+
SpanType.new("rb:array", ->(value) { value.is_a?(Array) }, method(:describe_array), method(:rebuild_array)),
|
|
571
|
+
SpanType.new("rb:hash", ->(value) { value.is_a?(Hash) }, method(:describe_hash), method(:rebuild_hash)),
|
|
572
|
+
SpanType.new("rb:set", ->(value) { value.is_a?(Set) }, method(:describe_set), ->(built, _) { Set.new(built) }),
|
|
573
|
+
SpanType.new("rb:time", ->(value) { value.is_a?(Time) }, method(:describe_time), method(:rebuild_time)),
|
|
574
|
+
SpanType.new("rb:date", ->(value) { value.is_a?(Date) }, method(:describe_date), ->(built, _) { Date.iso8601(built) }),
|
|
575
|
+
SpanType.new("rb:bigdecimal", ->(value) { defined?(::BigDecimal) && value.is_a?(::BigDecimal) }, method(:describe_bigdecimal), ->(built, _) { BigDecimal(built) }),
|
|
576
|
+
SpanType.new("rb:struct", ->(value) { value.is_a?(Struct) }, method(:describe_struct), method(:rebuild_struct))
|
|
577
|
+
].freeze
|
|
578
|
+
|
|
579
|
+
SPAN_TYPES_BY_NAME = SPAN_TYPES.to_h { |span_type| [span_type.name, span_type] }.freeze
|
|
580
|
+
|
|
581
|
+
class Exact
|
|
582
|
+
def initialize(lossy)
|
|
583
|
+
@lossy = lossy
|
|
584
|
+
@tracks_paths = !lossy.empty?
|
|
585
|
+
@forward = {}.compare_by_identity
|
|
586
|
+
@backward = {}.compare_by_identity
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def same?(a, b, path)
|
|
590
|
+
return true if @tracks_paths && @lossy.include?(path)
|
|
591
|
+
return false unless a.instance_of?(b.class)
|
|
592
|
+
|
|
593
|
+
if SpanMeta.tracks_identity?(a)
|
|
594
|
+
return false unless @forward.fetch(a, b).equal?(b) && @backward.fetch(b, a).equal?(a)
|
|
595
|
+
return true if @forward.key?(a)
|
|
596
|
+
|
|
597
|
+
@forward[a] = b
|
|
598
|
+
@backward[b] = a
|
|
599
|
+
end
|
|
600
|
+
same_value?(a, b, path) && same_ivars?(a, b, path)
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
private
|
|
604
|
+
|
|
605
|
+
def same_value?(a, b, path)
|
|
606
|
+
case a
|
|
607
|
+
when Float then a == b || (a.nan? && b.nan?)
|
|
608
|
+
when String then a == b && (a.encoding == b.encoding || (a.ascii_only? && PLAIN_ENCODINGS.include?(a.encoding) && PLAIN_ENCODINGS.include?(b.encoding)))
|
|
609
|
+
when Array then a.length == b.length && a.each_with_index.all? { |item, index| same?(item, b[index], child(path, index)) }
|
|
610
|
+
when Hash then same_hash?(a, b, path)
|
|
611
|
+
when Time then a == b && a.utc_offset == b.utc_offset && a.utc? == b.utc? && a.zone == b.zone
|
|
612
|
+
when Struct then a.members.all? { |member| same?(a[member], b[member], child(path, member)) }
|
|
613
|
+
else
|
|
614
|
+
a.method(:==).owner == BasicObject || a == b
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def same_hash?(a, b, path)
|
|
619
|
+
a.keys == b.keys && a.default == b.default && a.default_proc == b.default_proc &&
|
|
620
|
+
a.compare_by_identity? == b.compare_by_identity? &&
|
|
621
|
+
a.all? { |key, item| same?(item, b[key], child(path, key)) }
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def same_ivars?(a, b, path)
|
|
625
|
+
names = a.instance_variables
|
|
626
|
+
names == b.instance_variables &&
|
|
627
|
+
names.all? { |name| same?(a.instance_variable_get(name), b.instance_variable_get(name), child(path, name)) }
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def child(path, segment)
|
|
631
|
+
@tracks_paths ? path + [segment.to_s] : path
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|
data/lib/bitfab/version.rb
CHANGED
data/lib/bitfab.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "bitfab/warn_once"
|
|
|
8
8
|
require_relative "bitfab/experiment_id"
|
|
9
9
|
require_relative "bitfab/payload_budget"
|
|
10
10
|
require_relative "bitfab/serialize"
|
|
11
|
+
require_relative "bitfab/span_meta"
|
|
11
12
|
require_relative "bitfab/db_snapshot"
|
|
12
13
|
require_relative "bitfab/git_command"
|
|
13
14
|
require_relative "bitfab/commit_ref"
|
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.
|
|
4
|
+
version: 0.68.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Harvest Team
|
|
@@ -211,6 +211,7 @@ files:
|
|
|
211
211
|
- lib/bitfab/serialize.rb
|
|
212
212
|
- lib/bitfab/simulation_plan.rb
|
|
213
213
|
- lib/bitfab/span_context.rb
|
|
214
|
+
- lib/bitfab/span_meta.rb
|
|
214
215
|
- lib/bitfab/span_origin.rb
|
|
215
216
|
- lib/bitfab/spans.rb
|
|
216
217
|
- lib/bitfab/subtree.rb
|