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

Refactor breakpoints system #405

Merged
merged 10 commits into from
Jun 20, 2017
Merged

Refactor breakpoints system #405

merged 10 commits into from
Jun 20, 2017

Conversation

vspinu
Copy link
Contributor

@vspinu vspinu commented May 30, 2017

This addresses clojure-emacs/cider#1699. The core of the change is to remove most of the global dynamic vars and rely on lexical binding on top of the main instrumented form for all the common metadata. Currently same metadata is replicated again and again for each sub-form. Another major change is to reduce the macros machinery to two basic function calls break and apply-instrumented-maybe.

This pretty much reaches theoretical minimum for the size of generated clojure code, one call per instrumented form. Pushing this further would require understanding lower level details of what and how eats into the java's 64KB method limit space.

The following code

(let [i 1]
  (+ i
     (* i i)))

now expands into:

(let*
 [META_
  {:code "#dbg\n(let [i 1]\n  (+ i\n     (* i i)))\n",
   :original-id "12",
   :file "/home/vspinu/tmp/tmp.clj",
   :line 10,
   :column 0}]
 (cider.nrepl.middleware.debug/break
  []
  (let*
   [i 1]
   (cider.nrepl.middleware.debug/break
    [2]
    (+
     (cider.nrepl.middleware.debug/break [2 1] i META_ {})
     (cider.nrepl.middleware.debug/break
      [2 2]
      (*
       (cider.nrepl.middleware.debug/break [2 2 1] i META_ {})
       (cider.nrepl.middleware.debug/break [2 2 2] i META_ {}))
      META_
      {}))
    META_
    {}))
  META_
  {}))

instead of the current:

(let*
 []
 (clojure.core/push-thread-bindings
  (clojure.core/hash-map
   #'cider.nrepl.middleware.debug/*skip-breaks*
   (let*
    [or__4238__auto__ cider.nrepl.middleware.debug/*skip-breaks*]
    (if or__4238__auto__ or__4238__auto__ (clojure.core/atom nil)))
   #'cider.nrepl.middleware.debug/*locals*
   {}
   #'cider.nrepl.middleware.debug/*extras*
   {:code "#dbg\n(let [i 1]\n  (+ i\n     (* i i)))\n",
    :original-id "20",
    :file "/home/vspinu/tmp/tmp.clj",
    :line 20,
    :column 0,
    :coor []}
   #'cider.nrepl.middleware.debug/*pprint-fn*
   (:pprint-fn
    clojure.tools.nrepl.middleware.interruptible-eval/*msg*)))
 (try
  (if
   (clojure.core/not
    (clojure.core/seq @cider.nrepl.middleware.debug/debugger-message))
   (do (throw (new java.lang.Exception "Debugger not initialized"))))
  (let*
   [val__6618__auto__
    (let*
     [i 1]
     (let*
      []
      (clojure.core/push-thread-bindings
       (clojure.core/hash-map
        #'cider.nrepl.middleware.debug/*skip-breaks*
        (let*
         
[or__4238__auto__ cider.nrepl.middleware.debug/*skip-breaks*]
         (if
          or__4238__auto__
          or__4238__auto__
          (clojure.core/atom nil)))
        #'cider.nrepl.middleware.debug/*locals*
        {}
        #'cider.nrepl.middleware.debug/*extras*
        {:code "#dbg\n(let [i 1]\n  (+ i\n     (* i i)))\n",
         :original-id "20",
         :file "/home/vspinu/tmp/tmp.clj",
         :line 20,
         :column 0,
         :coor [2]}
        #'cider.nrepl.middleware.debug/*pprint-fn*
        (:pprint-fn
         clojure.tools.nrepl.middleware.interruptible-eval/*msg*)))

.............................................................................
.............................................................................
clip 282 lines
.............................................................................
.............................................................................

   (if
    (cider.nrepl.middleware.debug/skip-breaks? [])
    val__6618__auto__
    (if
     (clojure.core/=
      (:mode @cider.nrepl.middleware.debug/*skip-breaks*)
      :trace)
     (do
      (cider.nrepl.middleware.debug/print-step-indented
       0
       '(let* [i 1] (+ i (* i i)))
       val__6618__auto__)
      val__6618__auto__)
     (if
      :else
      (cider.nrepl.middleware.debug/read-debug-command
       val__6618__auto__
       (clojure.core/assoc
        cider.nrepl.middleware.debug/*extras*

        :debug-value
        (cider.nrepl.middleware.debug/pr-short val__6618__auto__)))
      nil))))
  (finally (clojure.core/pop-thread-bindings))))

@bbatsov
Copy link
Member

bbatsov commented May 31, 2017

@vspinu Impressive work!

@Malabarba please, take a look at the changes.

@@ -253,6 +253,15 @@
;; Don't use `postwalk` because it destroys previous metadata.
(walk-indexed #(tag-form %2 breakfunction) form))

(defn print-form [form & [expand? meta?]]
Copy link
Member

Choose a reason for hiding this comment

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

You can add some docstring here.

@@ -137,8 +135,7 @@
(let [{extras ::extras,
[_ orig] ::original-form,
bf ::breakfunction} (meta form)]
(when verbose-debug
(println "[DBG]" (not (not bf)) extras (or orig form)))
;; (println "[DBG]" (not (not bf)) extras (or orig form))
Copy link
Member

Choose a reason for hiding this comment

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

I won't leave the commented out code here.

;; Top-level sexps are not debugged, just returned.
(is (= (eval `(let [~'x 10] (d/breakpoint d/*locals* {:coor []} nil)))
'{x 10})))))
;; (deftest breakpoint
Copy link
Member

Choose a reason for hiding this comment

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

Leaving this commented out was probably not intended

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right. Good catch.

@expez
Copy link
Member

expez commented May 31, 2017

Thank you so much for digging into this @vspinu!

I hit this all the time, but I haven't had to time to do anything about it.

@vspinu
Copy link
Contributor Author

vspinu commented May 31, 2017

One thing that I am not very clear about is why eval-with-locals uses the dynamic *ppprint-fn* (here). This doesn't seem necessary so I changed it to a more direct (:pprint-fn *msg*).

@vspinu
Copy link
Contributor Author

vspinu commented Jun 1, 2017

For the record. Two more factors contribute to the size of the method - original forms that are inlined into the final code and locals (&env expansion). If any of these two is large you can get "method too large" even with a simple #break.

The original forms could be saved in a separate atom with a simple macro trick, but locals really need to be inlined. So I am "fixing" the "locals issue" by simply recompiling without locals when "method code too large!" is caught. You won't be able to use commands that depend on locals (l,e,j,p) but all others will work as expected. It's a good tradeoff.

@dpsutton
Copy link
Contributor

dpsutton commented Jun 1, 2017

do you think it would be possible for you to put together a markdown document giving a general overview of how the debugger works? I'd imagine no one knows more about it that you right now.

@Malabarba
Copy link
Member

Thanks indeed @vspinu . I'll try to have a look as soon as possible.

@vspinu
Copy link
Contributor Author

vspinu commented Jun 1, 2017

a markdown document giving a general overview of how the debugger works?

I am not currently in the position to do that as I don't have a good understanding of the emacs-debuger communication. Some design choices are still not clear to me (for instance why compulsory initialization?, why blocking debugger?). This PR mostly concerns with the breakpoint logic.

Only @Malabarba could make such a write-up but I am not sure it would be worth it at this stage. The debugger is still a moving target and there are already quite a few documentation notes in the code.

@grammati
Copy link
Contributor

grammati commented Jun 2, 2017

For what it's worth, I have been trying out this change and it works perfectly, as far as I can tell.

I have tried out all the debug operations, and have not seen any regressions. I have also tried debugging functions that used to fail with "Method code too large", and they can be debugged now. Yay!

👍

@Malabarba
Copy link
Member

WRT a markdown guide: there's an issue on this repo (or maybe on the cider repo) about supporting cljs in the debugger. On that issue I made a very short guise on how the debugger works.
I could write a more completed one if I ever find the time.

[a b]
(= (seq a) (seq b)))


Copy link
Member

Choose a reason for hiding this comment

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

Whitespace

(let [br (::breakfunction (meta f))
f1 (m/strip-meta f keys)]
(if br
(vary-meta f1 assoc :instrumented (name (:name (meta br))))
Copy link
Member

Choose a reason for hiding this comment

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

Why use (name (:name (meta br))) instead of just br?

Copy link
Member

Choose a reason for hiding this comment

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

Also, we should use ::instrumented.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Final form has only :instrumented string tag in it. Keeping full function around leads to huge clutter when you print the form with meta and generally makes it much harder to understand what is what. Each meta key also adds 5 extra bytecode ops to the final object size on every breakpoint invocation. After all it's just a tag and should be a string or a symbol, especially when you send it to emacs.

The non-qualified :instruented is intentional. I am not bound to it but it looks to me that qualification is not warranted in this context. The:instrumented tag is more generic than :cider.nrepl.middleware.util.instrument/instrumented. Different middleware or different implementation of instrumentation should be able to use it without sending that sausage around. It also rather annoying in printouts as doesn't bear extra informational content there. On emacs side it feels like spillage of internals as well. See associated changes.

(->> (pr-very-short out#)
(assoc d/*extras* :status :enlighten :debug-value)
d/debugger-send))
(->> (assoc (:msg_ ~'META_)
Copy link
Member

Choose a reason for hiding this comment

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

Is the _ in :msg_ intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Typo indeed.

@Malabarba
Copy link
Member

Besides a few comments, I think it looks good. And it must have taken a lot of work. Thanks a lot for the effort.

@Malabarba
Copy link
Member

Also what's that META_ symbol you are using? Is that some syntax I never heard of?

@bbatsov
Copy link
Member

bbatsov commented Jun 8, 2017

@vspinu I'll merge this once you address @Malabarba's comments. And you might also want to combine some of the commits together, but that's not particularly important to me.

@vspinu
Copy link
Contributor Author

vspinu commented Jun 8, 2017

Also what's that META_ symbol you are using?

It's just an anaphoric local variable. It needs to be accessed from all breakpoint macros and as clojure autoqualifies symbols in macros you need to inhibit the auto-qualification by quoting with ~' everywhere. I appended _ at the end to signify that it's an internal symbol which is automatically cleaned up in sanitize-env along all clojure temporaries with _ in them. Also to avoid potential conflicts with user variables named META.

BTW, that cleaning of local variables will break if users use locals with _, but that's not really a regression.

@vspinu
Copy link
Contributor Author

vspinu commented Jun 8, 2017

cleaning of local variables will break if users use locals with _,

Could be improved by removing symbols with __ only. AFAICS all clojure temporaries use that. So I guess I will also use META__ then.

@bbatsov
Copy link
Member

bbatsov commented Jun 9, 2017

Taht could be robustified by removing symbols with __ only. AFAICS all clojure temporaries use that. So I guess I will also use META__ then.

Sounds good. Also add some explanations about this in the code itself.

@bbatsov
Copy link
Member

bbatsov commented Jun 14, 2017

@vspinu ping :-)

@vspinu
Copy link
Contributor Author

vspinu commented Jun 14, 2017

Yes. I will be back on it this weekend. Got a rather tight two weeks.

@bbatsov
Copy link
Member

bbatsov commented Jun 18, 2017

OK. You'll also have to rebase this on top of the current master.

@xiongtx
Copy link
Member

xiongtx commented Jun 18, 2017

I don't understand the changes, but the new expansion seems much more pleasing than the old 😄.

vspinu added 2 commits June 18, 2017 18:03
  - Common metadata is now let bound once at the top of the form.
  - Macros expand into parsimonious 'break' and `apply-instrumented-maybe`
    functions. This bring the size of the expanded code to one call per
    breakpoint.
  - Only `coor` is hooked in local `extras` metadata
  - Remove verbose-debug. It's much less useful now as there are more points of
    interests. It's easier to insert `print-form` whenever needed.
@vspinu vspinu mentioned this pull request Jun 18, 2017
5 tasks
vspinu added 6 commits June 18, 2017 20:24
  - save original forms within local map
  - recompile with no &env bindings when "code too large!" method detected
Reasons:

 - Each meta keywors shows 5 java ops in bytecode

 - Inspection of code with functions in metadata is a nightmare
@vspinu vspinu force-pushed the break branch 3 times, most recently from 71cdb34 to 650d8af Compare June 18, 2017 19:35
vspinu added 2 commits June 18, 2017 21:41
  - Rename META_ into META__ and document
  - Only strip local variables with __ in their names
  - Use :cider/instrumented as instrumentation tag
  - Document defonces
  - Move *skip-breaks* reset to wrap-debug to make the intention clearer
  - Update enlighten to nested META_
  - Rename resolve-special -> special-sym-meta
  - Don't check for special-symbol? explicitly but rely on special-sym-meta instead
  - Remove unnecessary duplicated computation in info-clj
@vspinu
Copy link
Contributor Author

vspinu commented Jun 18, 2017

I am done with addressing all comments and rebasing & squashing it. Added one more "side" commit relating to #410.

One cell in travis matrix fails for non-sider reason.

@bbatsov bbatsov merged commit 0406734 into clojure-emacs:master Jun 20, 2017
@bbatsov
Copy link
Member

bbatsov commented Jun 20, 2017

👍

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

Successfully merging this pull request may close these issues.

7 participants