ddtrace 1.17.0 → 1.19.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/CHANGELOG.md +85 -2
- data/ext/ddtrace_profiling_native_extension/clock_id_from_pthread.c +3 -0
- data/ext/ddtrace_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +67 -52
- data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.c +22 -14
- data/ext/ddtrace_profiling_native_extension/collectors_dynamic_sampling_rate.h +4 -0
- data/ext/ddtrace_profiling_native_extension/collectors_gc_profiling_helper.c +156 -0
- data/ext/ddtrace_profiling_native_extension/collectors_gc_profiling_helper.h +5 -0
- data/ext/ddtrace_profiling_native_extension/collectors_stack.c +43 -102
- data/ext/ddtrace_profiling_native_extension/collectors_stack.h +10 -3
- data/ext/ddtrace_profiling_native_extension/collectors_thread_context.c +167 -125
- data/ext/ddtrace_profiling_native_extension/collectors_thread_context.h +2 -1
- data/ext/ddtrace_profiling_native_extension/extconf.rb +44 -10
- data/ext/ddtrace_profiling_native_extension/heap_recorder.c +970 -0
- data/ext/ddtrace_profiling_native_extension/heap_recorder.h +155 -0
- data/ext/ddtrace_profiling_native_extension/helpers.h +2 -0
- data/ext/ddtrace_profiling_native_extension/http_transport.c +5 -2
- data/ext/ddtrace_profiling_native_extension/libdatadog_helpers.c +20 -0
- data/ext/ddtrace_profiling_native_extension/libdatadog_helpers.h +11 -0
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.c +83 -18
- data/ext/ddtrace_profiling_native_extension/private_vm_api_access.h +6 -0
- data/ext/ddtrace_profiling_native_extension/profiling.c +2 -0
- data/ext/ddtrace_profiling_native_extension/ruby_helpers.c +147 -0
- data/ext/ddtrace_profiling_native_extension/ruby_helpers.h +28 -0
- data/ext/ddtrace_profiling_native_extension/stack_recorder.c +330 -13
- data/ext/ddtrace_profiling_native_extension/stack_recorder.h +3 -0
- data/lib/datadog/appsec/component.rb +4 -1
- data/lib/datadog/appsec/configuration/settings.rb +4 -0
- data/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb +2 -0
- data/lib/datadog/appsec/processor/rule_loader.rb +60 -0
- data/lib/datadog/appsec/remote.rb +12 -9
- data/lib/datadog/core/configuration/settings.rb +139 -22
- data/lib/datadog/core/configuration.rb +4 -0
- data/lib/datadog/core/remote/worker.rb +1 -0
- data/lib/datadog/core/telemetry/collector.rb +10 -0
- data/lib/datadog/core/telemetry/event.rb +2 -1
- data/lib/datadog/core/telemetry/ext.rb +3 -0
- data/lib/datadog/core/telemetry/v1/app_event.rb +8 -1
- data/lib/datadog/core/telemetry/v1/install_signature.rb +38 -0
- data/lib/datadog/core/workers/async.rb +1 -0
- data/lib/datadog/kit/enable_core_dumps.rb +5 -6
- data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +7 -11
- data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +1 -0
- data/lib/datadog/profiling/component.rb +210 -18
- data/lib/datadog/profiling/scheduler.rb +4 -6
- data/lib/datadog/profiling/stack_recorder.rb +13 -2
- data/lib/datadog/tracing/contrib/mysql2/configuration/settings.rb +4 -0
- data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +2 -1
- data/lib/datadog/tracing/contrib/pg/configuration/settings.rb +5 -0
- data/lib/datadog/tracing/contrib/pg/instrumentation.rb +24 -0
- data/lib/datadog/tracing/contrib/rails/auto_instrument_railtie.rb +0 -2
- data/lib/datadog/tracing/workers.rb +1 -0
- data/lib/ddtrace/version.rb +1 -1
- metadata +11 -6
@@ -314,32 +314,85 @@ module Datadog
|
|
314
314
|
|
315
315
|
# Can be used to enable/disable the Datadog::Profiling.allocation_count feature.
|
316
316
|
#
|
317
|
-
# This feature is
|
317
|
+
# This feature is now controlled via {:experimental_allocation_enabled}
|
318
|
+
option :allocation_counting_enabled do |o|
|
319
|
+
o.after_set do
|
320
|
+
Datadog.logger.warn(
|
321
|
+
'The profiling.advanced.allocation_counting_enabled setting has been deprecated for removal and no ' \
|
322
|
+
'longer does anything. Please remove it from your Datadog.configure block. ' \
|
323
|
+
'Allocation counting is now controlled by the `experimental_allocation_enabled` setting instead.'
|
324
|
+
)
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
# Can be used to enable/disable collection of allocation profiles.
|
329
|
+
#
|
330
|
+
# This feature is alpha and disabled by default
|
318
331
|
#
|
319
|
-
#
|
320
|
-
#
|
321
|
-
#
|
322
|
-
# (https://bugs.ruby-lang.org/issues/18464). We don't recommend using this feature on such Rubies.
|
323
|
-
# This bug is fixed on Ruby versions 3.1.4, 3.2.3 and 3.3.0.
|
332
|
+
# @warn This feature is not supported/safe in all Rubies. Details in {Datadog::Profiling::Component} but
|
333
|
+
# in summary, this should be supported on Ruby 2.x, 3.1.4+, 3.2.3+ and 3.3.0+. Enabling it on
|
334
|
+
# unsupported Rubies may result in unexpected behaviour, including crashes.
|
324
335
|
#
|
325
|
-
#
|
326
|
-
|
327
|
-
|
328
|
-
|
336
|
+
# @default `DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED` environment variable as a boolean, otherwise `false`
|
337
|
+
option :experimental_allocation_enabled do |o|
|
338
|
+
o.type :bool
|
339
|
+
o.env 'DD_PROFILING_EXPERIMENTAL_ALLOCATION_ENABLED'
|
340
|
+
o.default false
|
341
|
+
end
|
342
|
+
|
343
|
+
# Can be used to enable/disable the collection of heap profiles.
|
329
344
|
#
|
330
|
-
#
|
331
|
-
# Ruby 3.2.0 to 3.2.2 have a bug in the newobj tracepoint (https://bugs.ruby-lang.org/issues/19482,
|
332
|
-
# https://github.com/ruby/ruby/pull/7464) so that's an extra reason why it's not safe on those Rubies.
|
333
|
-
# This bug is fixed on Ruby versions 3.2.3 and 3.3.0.
|
345
|
+
# This feature is alpha and disabled by default
|
334
346
|
#
|
335
|
-
# @
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
347
|
+
# @warn To enable heap profiling you are required to also enable allocation profiling.
|
348
|
+
#
|
349
|
+
# @default `DD_PROFILING_EXPERIMENTAL_HEAP_ENABLED` environment variable as a boolean, otherwise `false`
|
350
|
+
option :experimental_heap_enabled do |o|
|
351
|
+
o.type :bool
|
352
|
+
o.env 'DD_PROFILING_EXPERIMENTAL_HEAP_ENABLED'
|
353
|
+
o.default false
|
354
|
+
end
|
355
|
+
|
356
|
+
# Can be used to enable/disable the collection of heap size profiles.
|
357
|
+
#
|
358
|
+
# This feature is alpha and enabled by default when heap profiling is enabled.
|
359
|
+
#
|
360
|
+
# @warn To enable heap size profiling you are required to also enable allocation and heap profiling.
|
361
|
+
#
|
362
|
+
# @default `DD_PROFILING_EXPERIMENTAL_HEAP_SIZE_ENABLED` environment variable as a boolean, otherwise
|
363
|
+
# whatever the value of DD_PROFILING_EXPERIMENTAL_HEAP_ENABLED is.
|
364
|
+
option :experimental_heap_size_enabled do |o|
|
365
|
+
o.type :bool
|
366
|
+
o.env 'DD_PROFILING_EXPERIMENTAL_HEAP_SIZE_ENABLED'
|
367
|
+
o.default true # This gets ANDed with experimental_heap_enabled in the profiler component.
|
368
|
+
end
|
369
|
+
|
370
|
+
# Can be used to configure the allocation sampling rate: a sample will be collected every x allocations.
|
371
|
+
#
|
372
|
+
# The lower the value, the more accuracy in allocation and heap tracking but the bigger the overhead. In
|
373
|
+
# particular, a value of 1 will sample ALL allocations.
|
374
|
+
#
|
375
|
+
# @default `DD_PROFILING_EXPERIMENTAL_ALLOCATION_SAMPLE_RATE` environment variable, otherwise `50`.
|
376
|
+
option :experimental_allocation_sample_rate do |o|
|
377
|
+
o.type :int
|
378
|
+
o.env 'DD_PROFILING_EXPERIMENTAL_ALLOCATION_SAMPLE_RATE'
|
379
|
+
o.default 50
|
380
|
+
end
|
381
|
+
|
382
|
+
# Can be used to configure the heap sampling rate: a heap sample will be collected for every x allocation
|
383
|
+
# samples.
|
384
|
+
#
|
385
|
+
# The lower the value, the more accuracy in heap tracking but the bigger the overhead. In particular, a
|
386
|
+
# value of 1 will track ALL allocations samples for heap profiles.
|
387
|
+
#
|
388
|
+
# The effective heap sampling rate in terms of allocations (not allocation samples) can be calculated via
|
389
|
+
# effective_heap_sample_rate = allocation_sample_rate * heap_sample_rate.
|
390
|
+
#
|
391
|
+
# @default `DD_PROFILING_EXPERIMENTAL_HEAP_SAMPLE_RATE` environment variable, otherwise `10`.
|
392
|
+
option :experimental_heap_sample_rate do |o|
|
393
|
+
o.type :int
|
394
|
+
o.env 'DD_PROFILING_EXPERIMENTAL_HEAP_SAMPLE_RATE'
|
395
|
+
o.default 10
|
343
396
|
end
|
344
397
|
|
345
398
|
# Can be used to disable checking which version of `libmysqlclient` is being used by the `mysql2` gem.
|
@@ -391,6 +444,34 @@ module Datadog
|
|
391
444
|
end
|
392
445
|
end
|
393
446
|
end
|
447
|
+
|
448
|
+
# Configures how much wall-time overhead the profiler targets. The profiler will dynamically adjust the
|
449
|
+
# interval between samples it takes so as to try and maintain the property that it spends no longer than
|
450
|
+
# this amount of wall-clock time profiling. For example, with the default value of 2%, the profiler will
|
451
|
+
# try and cause no more than 1.2 seconds per minute of overhead. Decreasing this value will reduce the
|
452
|
+
# accuracy of the data collected. Increasing will impact the application.
|
453
|
+
#
|
454
|
+
# We do not recommend tweaking this value.
|
455
|
+
#
|
456
|
+
# This value should be a percentage i.e. a number between 0 and 100, not 0 and 1.
|
457
|
+
#
|
458
|
+
# @default `DD_PROFILING_OVERHEAD_TARGET_PERCENTAGE` as a float, otherwise 2.0
|
459
|
+
option :overhead_target_percentage do |o|
|
460
|
+
o.type :float
|
461
|
+
o.env 'DD_PROFILING_OVERHEAD_TARGET_PERCENTAGE'
|
462
|
+
o.default 2.0
|
463
|
+
end
|
464
|
+
|
465
|
+
# Controls how often the profiler reports data, in seconds. Cannot be lower than 60 seconds.
|
466
|
+
#
|
467
|
+
# We do not recommend tweaking this value.
|
468
|
+
#
|
469
|
+
# @default `DD_PROFILING_UPLOAD_PERIOD` environment variable, otherwise 60
|
470
|
+
option :upload_period_seconds do |o|
|
471
|
+
o.type :int
|
472
|
+
o.env 'DD_PROFILING_UPLOAD_PERIOD'
|
473
|
+
o.default 60
|
474
|
+
end
|
394
475
|
end
|
395
476
|
|
396
477
|
# @public_api
|
@@ -592,6 +673,42 @@ module Datadog
|
|
592
673
|
o.env Core::Telemetry::Ext::ENV_HEARTBEAT_INTERVAL
|
593
674
|
o.default 60.0
|
594
675
|
end
|
676
|
+
|
677
|
+
# The install id of the application.
|
678
|
+
#
|
679
|
+
# This method is used internally, by library injection.
|
680
|
+
#
|
681
|
+
# @default `DD_INSTRUMENTATION_INSTALL_ID` environment variable, otherwise `nil`.
|
682
|
+
# @return [String,nil]
|
683
|
+
# @!visibility private
|
684
|
+
option :install_id do |o|
|
685
|
+
o.type :string, nilable: true
|
686
|
+
o.env Core::Telemetry::Ext::ENV_INSTALL_ID
|
687
|
+
end
|
688
|
+
|
689
|
+
# The install type of the application.
|
690
|
+
#
|
691
|
+
# This method is used internally, by library injection.
|
692
|
+
#
|
693
|
+
# @default `DD_INSTRUMENTATION_INSTALL_TYPE` environment variable, otherwise `nil`.
|
694
|
+
# @return [String,nil]
|
695
|
+
# @!visibility private
|
696
|
+
option :install_type do |o|
|
697
|
+
o.type :string, nilable: true
|
698
|
+
o.env Core::Telemetry::Ext::ENV_INSTALL_TYPE
|
699
|
+
end
|
700
|
+
|
701
|
+
# The install time of the application.
|
702
|
+
#
|
703
|
+
# This method is used internally, by library injection.
|
704
|
+
#
|
705
|
+
# @default `DD_INSTRUMENTATION_INSTALL_TIME` environment variable, otherwise `nil`.
|
706
|
+
# @return [String,nil]
|
707
|
+
# @!visibility private
|
708
|
+
option :install_time do |o|
|
709
|
+
o.type :string, nilable: true
|
710
|
+
o.env Core::Telemetry::Ext::ENV_INSTALL_TIME
|
711
|
+
end
|
595
712
|
end
|
596
713
|
|
597
714
|
# Remote configuration
|
@@ -273,6 +273,10 @@ module Datadog
|
|
273
273
|
def handle_interrupt_shutdown!
|
274
274
|
logger = Datadog.logger
|
275
275
|
shutdown_thread = Thread.new { shutdown! }
|
276
|
+
unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
|
277
|
+
shutdown_thread.name = Datadog::Core::Configuration.name
|
278
|
+
end
|
279
|
+
|
276
280
|
print_message_treshold_seconds = 0.2
|
277
281
|
|
278
282
|
slow_shutdown = shutdown_thread.join(print_message_treshold_seconds).nil?
|
@@ -9,6 +9,7 @@ require_relative '../utils/hash'
|
|
9
9
|
require_relative 'v1/application'
|
10
10
|
require_relative 'v1/dependency'
|
11
11
|
require_relative 'v1/host'
|
12
|
+
require_relative 'v1/install_signature'
|
12
13
|
require_relative 'v1/integration'
|
13
14
|
require_relative 'v1/product'
|
14
15
|
require_relative '../transport/ext'
|
@@ -81,6 +82,15 @@ module Datadog
|
|
81
82
|
)
|
82
83
|
end
|
83
84
|
|
85
|
+
# Forms a telemetry app-started install_signature object
|
86
|
+
def install_signature
|
87
|
+
Telemetry::V1::InstallSignature.new(
|
88
|
+
install_id: Datadog.configuration.dig('telemetry', 'install_id'),
|
89
|
+
install_type: Datadog.configuration.dig('telemetry', 'install_type'),
|
90
|
+
install_time: Datadog.configuration.dig('telemetry', 'install_time'),
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
84
94
|
# Forms a telemetry app-started integrations object
|
85
95
|
def integrations
|
86
96
|
Datadog.registry.map do |integration|
|
@@ -6,6 +6,9 @@ module Datadog
|
|
6
6
|
module Ext
|
7
7
|
ENV_ENABLED = 'DD_INSTRUMENTATION_TELEMETRY_ENABLED'
|
8
8
|
ENV_HEARTBEAT_INTERVAL = 'DD_TELEMETRY_HEARTBEAT_INTERVAL'
|
9
|
+
ENV_INSTALL_ID = 'DD_INSTRUMENTATION_INSTALL_ID'
|
10
|
+
ENV_INSTALL_TYPE = 'DD_INSTRUMENTATION_INSTALL_TYPE'
|
11
|
+
ENV_INSTALL_TIME = 'DD_INSTRUMENTATION_INSTALL_TIME'
|
9
12
|
end
|
10
13
|
end
|
11
14
|
end
|
@@ -10,18 +10,24 @@ module Datadog
|
|
10
10
|
:additional_payload,
|
11
11
|
:configuration,
|
12
12
|
:dependencies,
|
13
|
+
:install_signature,
|
13
14
|
:integrations
|
14
15
|
|
15
16
|
# @param additional_payload [Array<Telemetry::V1::Configuration>] List of Additional payload to track (any key
|
16
17
|
# value not mentioned and doesn't fit under a metric)
|
17
18
|
# @param configuration [Array<Telemetry::V1::Configuration>] List of Tracer related configuration data
|
18
19
|
# @param dependencies [Array<Telemetry::V1::Dependency>] List of all loaded modules requested by the app
|
20
|
+
# @param install_signature [Telemetry::V1::InstallSignature] Install signature data
|
19
21
|
# @param integrations [Array<Telemetry::V1::Integration>] List of integrations that are available within the app
|
20
22
|
# and applicable to be traced
|
21
|
-
def initialize(
|
23
|
+
def initialize(
|
24
|
+
additional_payload: nil, configuration: nil, dependencies: nil, install_signature: nil,
|
25
|
+
integrations: nil
|
26
|
+
)
|
22
27
|
@additional_payload = additional_payload
|
23
28
|
@configuration = configuration
|
24
29
|
@dependencies = dependencies
|
30
|
+
@install_signature = install_signature
|
25
31
|
@integrations = integrations
|
26
32
|
end
|
27
33
|
|
@@ -30,6 +36,7 @@ module Datadog
|
|
30
36
|
hash[:additional_payload] = map_hash(@additional_payload) if @additional_payload
|
31
37
|
hash[:configuration] = map_hash(@configuration) if @configuration
|
32
38
|
hash[:dependencies] = map_array(@dependencies) if @dependencies
|
39
|
+
hash[:install_signature] = @install_signature.to_h if @install_signature
|
33
40
|
hash[:integrations] = map_array(@integrations) if @integrations
|
34
41
|
end
|
35
42
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
module Core
|
5
|
+
module Telemetry
|
6
|
+
module V1
|
7
|
+
# Describes attributes for install signature
|
8
|
+
class InstallSignature
|
9
|
+
using Core::Utils::Hash::Refinement
|
10
|
+
|
11
|
+
attr_reader \
|
12
|
+
:install_id,
|
13
|
+
:install_type,
|
14
|
+
:install_time
|
15
|
+
|
16
|
+
# @param id [String,nil] Install ID
|
17
|
+
# @param type [String,nil] Install type
|
18
|
+
# @param type [String,nil] Install time
|
19
|
+
def initialize(install_id:, install_type:, install_time:)
|
20
|
+
@install_id = install_id
|
21
|
+
@install_type = install_type
|
22
|
+
@install_time = install_time
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
hash = {
|
27
|
+
install_id: @install_id,
|
28
|
+
install_type: @install_type,
|
29
|
+
install_time: @install_time
|
30
|
+
}
|
31
|
+
hash.compact!
|
32
|
+
hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -16,11 +16,13 @@ module Datadog
|
|
16
16
|
'(Could not open /proc/sys/kernel/core_pattern)'
|
17
17
|
end
|
18
18
|
|
19
|
+
enabled_status = "Maximum size: #{maximum_size} Output pattern: '#{core_pattern}'"
|
20
|
+
|
19
21
|
if maximum_size <= 0
|
20
22
|
Kernel.warn("[ddtrace] Could not enable core dumps on crash, maximum size is #{maximum_size} (disabled).")
|
21
23
|
return
|
22
24
|
elsif maximum_size == current_size
|
23
|
-
Kernel.warn(
|
25
|
+
Kernel.warn("[ddtrace] Core dumps already enabled, nothing to do. #{enabled_status}")
|
24
26
|
return
|
25
27
|
end
|
26
28
|
|
@@ -35,12 +37,9 @@ module Datadog
|
|
35
37
|
end
|
36
38
|
|
37
39
|
if current_size == 0
|
38
|
-
Kernel.warn("[ddtrace] Enabled core dumps.
|
40
|
+
Kernel.warn("[ddtrace] Enabled core dumps. #{enabled_status}")
|
39
41
|
else
|
40
|
-
Kernel.warn(
|
41
|
-
"[ddtrace] Raised core dump limit. Old size: #{current_size} " \
|
42
|
-
"Maximum size: #{maximum_size} Output pattern: '#{core_pattern}'"
|
43
|
-
)
|
42
|
+
Kernel.warn("[ddtrace] Raised core dump limit. Old size: #{current_size} #{enabled_status}")
|
44
43
|
end
|
45
44
|
end
|
46
45
|
end
|
@@ -15,14 +15,15 @@ module Datadog
|
|
15
15
|
|
16
16
|
def initialize(
|
17
17
|
gc_profiling_enabled:,
|
18
|
-
allocation_counting_enabled:,
|
19
18
|
no_signals_workaround_enabled:,
|
20
19
|
thread_context_collector:,
|
21
|
-
|
20
|
+
dynamic_sampling_rate_overhead_target_percentage:,
|
21
|
+
allocation_sample_every:,
|
22
|
+
allocation_profiling_enabled:,
|
22
23
|
# **NOTE**: This should only be used for testing; disabling the dynamic sampling rate will increase the
|
23
24
|
# profiler overhead!
|
24
25
|
dynamic_sampling_rate_enabled: true,
|
25
|
-
|
26
|
+
idle_sampling_helper: IdleSamplingHelper.new
|
26
27
|
)
|
27
28
|
unless dynamic_sampling_rate_enabled
|
28
29
|
Datadog.logger.warn(
|
@@ -30,22 +31,16 @@ module Datadog
|
|
30
31
|
)
|
31
32
|
end
|
32
33
|
|
33
|
-
if allocation_counting_enabled && allocation_sample_every > 0
|
34
|
-
Datadog.logger.warn(
|
35
|
-
"Enabled experimental allocation profiling: allocation_sample_every=#{allocation_sample_every}. This is " \
|
36
|
-
'experimental, not recommended, and will increase overhead!'
|
37
|
-
)
|
38
|
-
end
|
39
|
-
|
40
34
|
self.class._native_initialize(
|
41
35
|
self,
|
42
36
|
thread_context_collector,
|
43
37
|
gc_profiling_enabled,
|
44
38
|
idle_sampling_helper,
|
45
|
-
allocation_counting_enabled,
|
46
39
|
no_signals_workaround_enabled,
|
47
40
|
dynamic_sampling_rate_enabled,
|
41
|
+
dynamic_sampling_rate_overhead_target_percentage,
|
48
42
|
allocation_sample_every,
|
43
|
+
allocation_profiling_enabled,
|
49
44
|
)
|
50
45
|
@worker_thread = nil
|
51
46
|
@failure_exception = nil
|
@@ -78,6 +73,7 @@ module Datadog
|
|
78
73
|
end
|
79
74
|
end
|
80
75
|
@worker_thread.name = self.class.name # Repeated from above to make sure thread gets named asap
|
76
|
+
@worker_thread.thread_variable_set(:fork_safe, true)
|
81
77
|
end
|
82
78
|
|
83
79
|
true
|