Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share variable inspection logic between CDP and DAP #1001

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

amomchilov
Copy link

@amomchilov amomchilov commented Jul 26, 2023

The logic that decides how variables are presented is repeated between the CDP and DAP implementations. This is error prone and leads to inconsistencies, where improvements are made to one or the other, but not both. E.g.:

  1. The special #dump member for String only exists in VSCode DAP: show #dump when pp'ed string is too long #869
  2. This feature to hide special #class members only exists for VSCode DAP: add the ability to hide the #class item for each object #986
  3. VSCode formats hash keys with value_inspect, but not Chrome
  4. CDP adds #class appears at the end, whereas DAP adds #class to the start.

This PR extracts the variable inspection logic into a new VariableInspector class, which can be shared between VSCode and Chrome. This gives a central place to make future improvements.

I tried to make only the minimal behaviour changes necessary to unify the two implementations.

This PR then unlocks the ability to make some changes really nice/easily:

  1. Remove double colon in Hash keys (:sym: "value"sym: "value") #1003
  2. Omit #class for simple values #1004
  3. Add hook for customizing the debug representation of an object #1005

end

vars = members.map { |member| variable(member.name, member.value) }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's nothing implemented like CDP's internalProperty yet, but I think support for presentationHint can be added here eventually to achieve a similar effect.

Comment on lines 62 to 66
MAX_LENGTH = 180

def value_inspect obj, short: true
# TODO: max length should be configurable?
str = DEBUGGER__.safe_inspect obj, short: short, max_length: MAX_LENGTH

if str.encoding == Encoding::UTF_8
str.scrub
else
str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
end
end
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied this snippet for now, but my eventual goal is to move all presentation logic out of ThreadClient and into a shared class like this.

def named_members_of obj
members = case obj
when Array then obj.map.with_index { |o, i| Member.new(name: i.to_s, value: o) }
when Hash then obj.map { |k, v| Member.new(name: value_inspect(k), value: v) }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to be inconsistent before. VSCode would format Hash keys with value_inspect, but not Chrome. Now they will both us it.

result += M_INSTANCE_VARIABLES.bind_call(obj).map{|iv|
variable(iv, M_INSTANCE_VARIABLE_GET.bind_call(obj, iv))
}
prop += [internalProperty('#class', M_CLASS.bind_call(obj))]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Chrome, #class is appended as the last prop, whereas for VSCode it was unshifted to the top.

@amomchilov amomchilov force-pushed the share-variable-inspection-logic branch 4 times, most recently from 1295842 to 6119aa8 Compare July 27, 2023 20:41
@amomchilov amomchilov marked this pull request as ready for review July 27, 2023 20:50
@amomchilov amomchilov force-pushed the share-variable-inspection-logic branch 2 times, most recently from 34b1210 to de6411b Compare July 28, 2023 12:43
Comment on lines -876 to -879
obj = @var_map[vid]
if obj
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This had a small bug, where true would have its #class expanded, but not false or nil:

Screenshot 2023-07-28 at 8 28 00 AM

Notice that false and nil still have disclosure triangles. This is because of another bug, in variable_ (lines 1001-1002), which assumed that all objects would have at least 1 named member (#class), which wasn't the case here.

The new check fixes it:

Screenshot 2023-07-28 at 8 27 21 AM

Copy link
Author

@amomchilov amomchilov Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... this change allocates one more Member than before:

#<Member name="#class" value=NilClass internal>

Which bumps some of the variablesReference numbers in test/protocol/step_back_raw_dap_test.rb by 1.

See 63517f5

result: result_variable.inspect_value,
**render_variable(result_variable)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This result: value used to come from the variable_ method when name was nil. That was pretty tricky, so I moved it here

Comment on lines 1060 to 1069
namedVariables += M_INSTANCE_VARIABLES.bind_call(obj).size

if NaiveString === obj
str = obj.str.dump
vid = indexedVariables = namedVariables = 0
else
str = value_inspect(obj)
end
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just becomes a natural part of VariableInspecto#named_members_of, so we don't have to worry about it here anymore.

str = value_inspect(obj)
end

if name
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if name check is really checking if this is going to be used in a VariablesResponse (with a name:). If name is nil, then this was called from #evaluate_result for a EvaluateResponse (with a result:).

I moved this logic to the evaluate code path instead.

Comment on lines 1055 to 1050
variable[:indexedVariables] = indexedVariables unless indexedVariables == 0
variable[:namedVariables] = namedVariables unless namedVariables == 0
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of nice, it declutters the response logging a lot, so it's easier to debug.

Comment on lines -1093 to -1096
when String
variable_ name, obj, namedVariables: 3 # #length, #encoding, #to_str
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This separation between the code that answers "what are the members?" and "how many members are there?" made it quite difficult to make changes to the presentation objects correctly.

Now both have a single source of truth, in VariableInspector's #indexed_members_of and #named_members_of.

rescue Exception => e
"<#inspect raises #{e.inspect}>"
def self.safe_inspect obj, max_length: LimitedPP::SHORT_INSPECT_LENGTH, short: false
LimitedPP.safe_inspect(obj, max_length: max_length, short: short)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted this out so it can be used in VariableInspector (without needing to require the whole session.rb, such as in the unit test).

I left this stub to minimize the churn here

@ko1
Copy link
Collaborator

ko1 commented Sep 25, 2023

  • I like an idea to unify CDP and DAP because of implementation.
  • However I'm not sure we can merge it because of difference between CDP and DAP (I don't know the CDP details). Do you have a time to trouble shooting for CDP?
  • The name "variable.rb" doesn't make sense for me because it's only for the representation (the name is too general). debug.gem has a policy to reduce the number of "require" to reduce the boot time. Can we merge it with variable_inspector.rb?

@amomchilov
Copy link
Author

amomchilov commented Sep 26, 2023

Do you have a time to trouble shooting for CDP?

I've already run this with CDP, it works great.

Can we merge it with variable_inspector.rb?

@ko1 Done. I still call it class Variable. It's a general name, but I don't have any better ideas. Please suggest if you have any.

module DEBUGGER__
class VariableInspector
def indexed_members_of(obj, start:, count:)
return [] if start > (obj.length - 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this implementation, the #class will disappear when displaying Array. Is this intentional behavior?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense (in the same spirit as #1004), though perhaps we should keep the #class if it's a non-standard subclass of Array. I'll make that in a separate PR.

@ono-max ono-max self-requested a review December 10, 2023 07:31
@ono-max
Copy link
Member

ono-max commented Dec 10, 2023

Also, some tests are failed in protocol part. Can you fix them?
FYI: https://github.com/ruby/debug/blob/master/CONTRIBUTING.md

@amomchilov
Copy link
Author

@ono-max I can have a look over the holiday break next week :)

@amomchilov amomchilov force-pushed the share-variable-inspection-logic branch 2 times, most recently from a0a8b3b to 2e850bc Compare January 4, 2024 04:31
@amomchilov
Copy link
Author

Hey @ono-max, Happy new year!

I've fixed the protocol tests, and rebased onto the latest master.

There's 2 test failures in test/console/nested_break_test.rb left, but I don't see how they could be related.

@amomchilov amomchilov force-pushed the share-variable-inspection-logic branch from 2e850bc to 6837459 Compare January 4, 2024 04:42
internalProperty('#begin', obj.begin),
internalProperty('#end', obj.end),
]
if @obj_map.key?(oid)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I missed the edge case for falsy value.

def indexed_members_of(obj, start:, count:)
return [] if start > (obj.length - 1)

capped_count = [count, obj.length - start].min
Copy link
Member

@ono-max ono-max Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add the comment why we need to compare count and obj.length - start and choose the minimum one? Sorry, I could not understand the reason.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to keep the iteration from running off the end of the array. This used to give extra nils at the end. See 6837459

I can add a comment.

Could you please run the CI?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@ono-max
Copy link
Member

ono-max commented Jan 5, 2024

It seems to be that some protocol tests are still failed. Can you please check them? If you need help, please let me know 👍

I believe that https://github.com/ruby/debug/actions/runs/7405610000/job/20183471986?pr=1001 and https://github.com/ruby/debug/actions/runs/7405610000/job/20183471690?pr=1001 are not flakey tests.

@amomchilov
Copy link
Author

@ono-max Hmmm I think we have one of those "it works on my machine" situations

Say the line bart... sigh... it works on my machine.

$ bundle exec rake test_protocol

Loaded suite /Users/alex/.gem/ruby/3.3.0/gems/rake-13.0.6/lib/rake/rake_test_loader
Started
Finished in 1.438602 seconds.
--------------------------------------------------------------------------------
2 tests, 690 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
--------------------------------------------------------------------------------
1.39 tests/s, 479.63 assertions/s

I'm running on Ruby 3.3.0 on macOS Sonoma 14.2.1.

I suspect these errors are coming from the ancestors chains being in different in CI, but I don't know why that would be. Could you please help me investigate this? I don't have access to a Linux machine to compare against.

@ono-max
Copy link
Member

ono-max commented Jan 7, 2024

I investigated the cause of the failed tests. I believe that the reason is because ancestor classes are changed from Ruby 3.1. Thus, we can skip the test if the Ruby version is lower than 3.1, e.g. 6f154ac

@amomchilov
Copy link
Author

@ono-max Fantastic find! I pulled that commit into this PR, so you get credit for it :)

Is this ready to merge?

@ono-max
Copy link
Member

ono-max commented Jan 8, 2024

I appreciate your dedicated efforts and hard work. @ko1-san will merge this PR 👍

@amomchilov
Copy link
Author

@ono-max Thanks!

Could you run the CI one more time, so we're sure it's green before merging?

@amomchilov
Copy link
Author

amomchilov commented Jan 8, 2024

@ono-max I think the remaining test failures are just flakey tests. I can reproduce them Ruby 3.1.4, but they fail less than 1/10 times.

@amomchilov
Copy link
Author

@ko1 Can you please review and merge this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants