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

DrRacket non-responsive after generating complex picts using Rhombus #602

Open
samth opened this issue Dec 10, 2024 · 21 comments
Open

DrRacket non-responsive after generating complex picts using Rhombus #602

samth opened this issue Dec 10, 2024 · 21 comments

Comments

@samth
Copy link
Member

samth commented Dec 10, 2024

  1. Clone https://github.com/awsalmah/Y390 (at db5af715170ca79d184f190b273de5d29a65058e)
  2. Open https://github.com/awsalmah/Y390/blob/db5af715170ca79d184f190b273de5d29a65058e/11.%20ps11/ps11.rhm in DrRacket (tested on Mac and Linux and Windows)
  3. Run the file.
  4. Run draw_trip(generate_koch(100),turtle(40,100,0),empty_scene(200,200)) in the Interactions window
  5. Note that it takes quite a while, and after the image is printed it take a long time for the prompt to appear.
  6. DrRacket becomes highly non-responsive (to clicks, it's slow to run subsequent expressions in the Interactions Window) and this persists over time.

A few notes:

  1. Similar picts constructed in Racket with 2htdp/image do not have this behavior
  2. Minimizing the file to a single Rhombus file with just the relevant pict do not seem to have this behavior.
  3. Running the same things in racket-mode in Emacs does not seem to have this behavior.
  4. Memory usage becomes very high during this experiment (750 MB -> 1350 MB or more)
  5. Closing all tabs with #lang rhombus and clicking the GC button causes memory usage to revert to the previous level.
@samth
Copy link
Member Author

samth commented Dec 10, 2024

cc @awsalmah

@mflatt
Copy link
Member

mflatt commented Dec 10, 2024

I'm not seeing all the same behavior (not the memory use pattern), but I do see DrRacket as slow and unresponsive while the snowflake fragment is shown. I believe that's because DrRacket is running al the drawing commands of the pict when it updates; I expect Racket pict to behave the same way. Consistent with that understanding, adding .freeze() to the end of the draw_trip call avoids unresponsiveness.

The 2htdp/image representation model is a bitmap, not vector drawing as in a pict. And I imagine that racket-mode similarly coerces the pict to a bitmap just once, which amounts to the same thing as using freeze.

@samth
Copy link
Member Author

samth commented Dec 11, 2024

I tried implementing the same code in Racket using pict and do not see the slowdown or unresponsiveness. Below is the code I ran in Racket. But note that I also did not see the unresponsiveness with a single minimized Rhombus file.

#lang racket

(require pict)

(define (empty-scene x y) (rectangle x y))

(define (scene+line i x1 y1 x2 y2 c)
  (define l (pip-line (- x2 x1) (- y2 y1) 1))
  (pin-over i x1 y1 l))
; A Trip is [ListOf Step]

; A Step is one of:
; - (make-draw Number)
; - (make-turn Number)
; - (make-fork Trip)
; *Interpretation*: angle is how many degrees to turn left (counterclockwise)
(define-struct draw [distance] #:transparent)
(define-struct turn [angle] #:transparent)
(define-struct fork [child] #:transparent)

           

; A Turtle is (make-turtle Number Number Number)
; *Interpretation*: dir=0 faces east,
;                   dir=90 faces north,
;                   dir=180 faces west,
;                   dir=270 faces south
(define-struct turtle [x y dir] #:transparent)

(define (move s k)
  (cond [(draw? s)
         (make-turtle (+ (turtle-x k)
                         (* (draw-distance s)
                            (cos (turtle-rad k))))
                      (- (turtle-y k)
                         (* (draw-distance s)
                            (sin (turtle-rad k))))
                      (turtle-dir k))]
        [(turn? s)
         (make-turtle (turtle-x k)
                      (turtle-y k)
                      (+ (turtle-dir k) (turn-angle s)))]
        [(fork? s) k]))

(define (draw-step s k i)
  (cond [(draw? s)
         (scene+line i
                     (turtle-x k)
                     (turtle-y k)
                     (turtle-x (move s k))
                     (turtle-y (move s k))
                     "black")]
        [(turn? s) i]
        [(fork? s) (draw-trip (fork-child s) k i)]))

(define (draw-trip t k i)
  (cond [(empty? t) i]
        [(cons? t)
         (draw-trip (rest t)
                    (move (first t) k)
                    (draw-step (first t) k i))]))
(define (turtle-rad k)
  (* (turtle-dir k) (/ pi 180)))




; generate-koch : Number -> Trip
; Generate a Koch curve that overall moves forward by the given distance
; *Termination*: Dividing distance by 3 repeatedly makes it < 1 eventually
(define (generate-koch distance)
  (cond [(< distance 1)
         (list (make-draw distance))]
        [else
         (append (generate-koch (/ distance 3))
                 (list (make-turn 60))
                 (generate-koch (/ distance 3))
                 (list (make-turn -120))
                 (generate-koch (/ distance 3))
                 (list (make-turn 60))
                 (generate-koch (/ distance 3)))]))

(define (go) (draw-trip (generate-koch 100) (turtle 40 100 0) (empty-scene 200 200)))

@rfindler
Copy link
Member

I think that the drawing call that DrRacket does is based on the recorded datum so I'd expect slowdowns with anything that boils down to a pict. For example, with this program:

#lang racket
(require pict)
(for/fold ([p (filled-ellipse 10 10)])
          ([i (in-range 14)])
  (cc-superimpose p p))

I see a pause before the ellipse appears in the interactions window and when I add this printf:

diff --git a/drracket/drracket/private/pict-snip.rkt b/drracket/drracket/private/pict-snip.rkt
index ea767e6c..e582facf 100644
--- a/drracket/drracket/private/pict-snip.rkt
+++ b/drracket/drracket/private/pict-snip.rkt
@@ -30,6 +30,7 @@
       (set-box/f space a))
     (define proc #f)
     (define/override (draw dc x y left top right bottom dx dy draw-caret)
+      (printf "drawing recorded datum ~s\n" (string-length (format "~s" recorded-datum)))
       (unless proc
         (set! proc (with-handlers ((exn:fail? mk-error-drawer))
                      (recorded-datum->procedure recorded-datum))))

changing the argument to in-range seems to grow in the expected exponential way.

Maybe a good change would be for DrRacket to follow what Racket mode is doing and create a bitmap? @greghendershott can you point us to what Racket mode does for this kind of thing? Is it always creating a bitmap, maybe because that's the only way to get the image across to emacs in a format it can understand?

@greghendershott
Copy link
Contributor

@rfindler Racket Mode uses file/convertible like so:

https://github.com/greghendershott/racket-mode/blob/master/racket/image.rkt

  • When SVG is available (in a given Emacs, and, user prefers it), it prefers SVG over PNG bitmap. 1

  • Also prefers the bounded flavors of convertible, so width can be given to a pretty-print-size-hook.

  • It writes the SVG or PNG to a temp file, and outputs just the (path width) when marshaling from Racket back end to Emacs front end, because that was the original approach I borrowed and enhanced. But nowadays instead I could probably inline the image data. Certainly you could just keep it in memory, no temp file. 😄

Footnotes

  1. Would "freeze" to SVG be sufficient to avoid whatever causes the slowdown, and, can you render SVG? If so, might that be a happy balance between speed and space?

@rfindler
Copy link
Member

@greghendershott thanks! Based on the code below (showing that the svg can grow) and @samth 's reports about good performance in Racket Mode, I guess that the svg path isn't being taken for some reason.

I'm starting to think that DrRacket should always make the bitmap for the purposes of drawing the image (but it may make sense to keep the pict around for some other reason, like if someone wants to zoom in or something? not that that's currently supported). It may also make sense to do something with a size cutoff somehow, where we just give up on the drawing-commands-based formats in favor of a bitmap when there are too many drawing commands?

#lang racket
(require pict racket/gui/base)

(define (try depth)
  (define p
    (for/fold ([p (filled-ellipse 10 10)])
              ([i (in-range depth)])
      (cc-superimpose p p)))
  (define sp (open-output-string))
  (define svg-dc
    (new svg-dc%
         [width (pict-width p)]
         [height (pict-height p)]
         [output sp]))
  (send svg-dc start-doc "")
  (send svg-dc start-page)
  (draw-pict p svg-dc 0 0)
  (send svg-dc end-page)
  (send svg-dc end-doc)
  (string-length (get-output-string sp)))

(try 12)
(try 13)
(try 14)

@samth
Copy link
Member Author

samth commented Dec 11, 2024

I just tested this out on my racket-mode install and it's using SVG.

@samth
Copy link
Member Author

samth commented Dec 11, 2024

Also, even with 3 versions of @rfindler's ellipse program on the screen in DrRacket, the UI is quite responsive, entirely unlike the experience with the Rhombus program in the original bug report.

@rfindler
Copy link
Member

Also, even with 3 versions of @rfindler's ellipse program on the screen in DrRacket, the UI is quite responsive, entirely unlike the experience with the Rhombus program in the original bug report.

Even if you change the 14 to like 24 or 50?

@rfindler
Copy link
Member

Also (idea from Matthew), what happens if you turn off "populate compiled" in the details section of the language menu and you make sure that all .zo files are up to date (by running raco make on the command-line)?

@mflatt
Copy link
Member

mflatt commented Dec 11, 2024

Adding clip to the Racket pict version makes it slow like the Rhombus version (that uses clip):

(define (scene+line i x1 y1 x2 y2 c)
  (define l (pip-line (- x2 x1) (- y2 y1) 1))
  (clip (pin-over i x1 y1 l)))
;  ^^^^

Maybe there's room for improvement in the clip primitive.

rfindler added a commit to racket/pict that referenced this issue Dec 11, 2024
@rfindler
Copy link
Member

@samth : does that help with the original program? It helps with drawing the racketized version that has clip in it.

rfindler added a commit to racket/pict that referenced this issue Dec 11, 2024
try using the rectangle surrounding the clipping region to shortcircuit `clip`. That is,
when the rectangle that we would be clipping is outside the existing clipping region,
then we dont' need to actually do the clipping

related to racket/rhombus#602
@rfindler
Copy link
Member

rfindler commented Dec 11, 2024

Oops, I totally botched the previous attempt. I've tried again and it improves the code above (but with the call to clip added to it) adding this suffix to get timing numbers:

(time
 (let ()
   (define p (go))
   (define sp (open-output-string))
   (define svg-dc
     (new svg-dc%
          [width (pict-width p)]
          [height (pict-height p)]
          [output sp]))
   (send svg-dc start-doc "")
   (send svg-dc start-page)
   (draw-pict p svg-dc 0 0)
   (send svg-dc end-page)
   (send svg-dc end-doc)
   (string-length (get-output-string sp))))

@samth
Copy link
Member Author

samth commented Dec 11, 2024

The change to clip improves things a lot.

I still think the behavior before that change is odd in a way that suggests a problem somewhere.

Below is a screen recording of me using DrRacket (with populate compiled off) for the Racket version of the code, but with clip (and without @rfindler's change). As you can see in the video, just typing 1 in at the REPL takes many seconds and Racket seems to be spending a lot of time GCing. It's not clear to me what is going on there but it seems like much too much work is being done.

racket2.mp4

@rfindler
Copy link
Member

rfindler commented Dec 11, 2024

The change to clip improves things a lot.

I still think the behavior before that change is odd in a way that suggests a problem somewhere.

Apologies if this is too adam-and-eve-ish: the rough idea is that when a GUI event comes in, it is handled in its entirety before another one is handled. So when there is some unexpected performance problem on a callback that happens a lot, everything appears sluggish. In this case, redrawing the window is going to happen on a single callback and that single callback is going to call into the code that doesn't have the performance improvement in clip. So everything in the UI will seem sluggish as we'll get a lot of redraws clogging up the queue and your keystrokes or whatever else other things you're doing (moving the mouse, say) will take a long time to get to the front of the queue to be handled.

Generally speaking, we try not to have things that the user's program produces ever get time on the DrRacket eventspace's handler thread but, in this case, the data structure representing the pict is generated by the user's program and then processed on the DrRacket eventspace handler thread. So those 1024 calls to the region's intersect method seem to be the thing that needs optimizing to make this program all reasonable again.

Still, you're right that if I were to create 2^32 little black circles all on top of each other, then DrRacket will take a long time to draw them. I'm not sure there's much we can do about that, except possibly caching the drawing with a bitmap object so it happens only once. We didn't take that path yet, as it doesn't seem to be required for the program that started this issue.

@samth
Copy link
Member Author

samth commented Dec 11, 2024

What I'm trying to understand is what exactly the events are that are resulting in expensive redraws. As you can see in the video, typing 1 or scrolling don't prompt expensive computation. But then when I hit enter after typing in 1 it's doing a lot of work. It seems in particular that adding new lines to the editor (that represents the interactions window) is quite expensive when the editor contains this complex pict, but just adding text doesn't. Is that recomputation on newlines necessary?

@rfindler
Copy link
Member

Oh, I don't know about that. When redraws get triggered (as opposed to cached bitmaps) is complex, I think. Also I just see a message "Video can't be played because the file is corrupt". I don't see the video.

@rfindler
Copy link
Member

rfindler commented Dec 11, 2024

Oh, but scrolling will use a cached bitmap (often but not always) and edits that stay on a single line only redraw the single line. Maybe those two optimizations explain what you're seeing?

@samth
Copy link
Member Author

samth commented Dec 11, 2024

It's very odd that you don't see the video; I just tried it in both Chrome and Firefox on both my desktop and my phone and it's always there.

@rfindler
Copy link
Member

It's very odd that you don't see the video; I just tried it in both Chrome and Firefox on both my desktop and my phone and it's always there.

I do see it in another browser (my firefox doesn't like it). I think that the timing of the sluggishness in the video is probably precisely the times when it tries to actually redraw the fractal.

@greghendershott
Copy link
Contributor

FWIW I get the same "video is corrupt" error in Firefox 132.0.1 on Fedora 40.

When I Save As and try to play the mp4 file directly from the Fedora/Gnome Videos app, I get "H.264 (High 4:4:4 Profile) decoder is required to play the file, but is not installed."

Similar error from VLC app "Codec not supported: VLC could not decode the format "h264" (H264 - MPEG-4 AVC (part 10))".

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

No branches or pull requests

4 participants