From b6f71ec4be19365631b377a505b331bf3f48e457 Mon Sep 17 00:00:00 2001 From: James Swaine Date: Wed, 29 Feb 2012 11:43:33 -0600 Subject: [PATCH] Add futures visualizer, improvements to futures logging --- collects/racket/future/private/constants.rkt | 47 + collects/racket/future/private/display.rkt | 126 +++ .../racket/future/private/drawing-helpers.rkt | 109 +++ .../racket/future/private/graph-drawing.rkt | 440 ++++++++++ .../racket/future/private/gui-helpers.rkt | 213 +++++ .../racket/future/private/visualizer-data.rkt | 365 ++++++++ .../future/private/visualizer-drawing.rkt | 831 ++++++++++++++++++ .../racket/future/private/visualizer-gui.rkt | 361 ++++++++ collects/racket/future/visualizer.rkt | 5 + collects/scribblings/guide/futures.scrbl | 398 +++++++-- collects/scribblings/guide/mand-bad-hover.png | Bin 0 -> 27421 bytes collects/scribblings/guide/mand-bad.png | Bin 0 -> 24129 bytes collects/scribblings/guide/mand-good.png | Bin 0 -> 25615 bytes collects/scribblings/guide/vis-main.png | Bin 0 -> 25082 bytes .../scribblings/reference/concurrency.scrbl | 1 + .../reference/futures-visualizer.scrbl | 86 ++ collects/scribblings/reference/futures.scrbl | 18 +- collects/tests/future/bad-trace1.rkt | 105 +++ collects/tests/future/future.rkt | 2 +- collects/tests/future/visualizer.rkt | 273 ++++++ qsort2.rkt | 98 +++ src/racket/src/future.c | 145 ++- src/racket/src/future.h | 2 +- 23 files changed, 3520 insertions(+), 105 deletions(-) create mode 100644 collects/racket/future/private/constants.rkt create mode 100644 collects/racket/future/private/display.rkt create mode 100644 collects/racket/future/private/drawing-helpers.rkt create mode 100644 collects/racket/future/private/graph-drawing.rkt create mode 100644 collects/racket/future/private/gui-helpers.rkt create mode 100644 collects/racket/future/private/visualizer-data.rkt create mode 100644 collects/racket/future/private/visualizer-drawing.rkt create mode 100644 collects/racket/future/private/visualizer-gui.rkt create mode 100644 collects/racket/future/visualizer.rkt create mode 100644 collects/scribblings/guide/mand-bad-hover.png create mode 100644 collects/scribblings/guide/mand-bad.png create mode 100644 collects/scribblings/guide/mand-good.png create mode 100644 collects/scribblings/guide/vis-main.png create mode 100644 collects/scribblings/reference/futures-visualizer.scrbl create mode 100644 collects/tests/future/bad-trace1.rkt create mode 100644 collects/tests/future/visualizer.rkt create mode 100644 qsort2.rkt diff --git a/collects/racket/future/private/constants.rkt b/collects/racket/future/private/constants.rkt new file mode 100644 index 0000000000..6bd3a1ab68 --- /dev/null +++ b/collects/racket/future/private/constants.rkt @@ -0,0 +1,47 @@ +#lang racket/base +(provide DEF-WINDOW-WIDTH + DEF-WINDOW-HEIGHT + RT-THREAD-ID + MIN-SEG-WIDTH + STROKE-WIDTH + MIN-SEG-INNER-WIDTH + DEFAULT-TIME-INTERVAL + TIMELINE-HEADER-OPACITY + CONNECTION-LINE-HAT-THRESHOLD + HAT-HEIGHT + CREATE-GRAPH-NODE-DIAMETER + CREATE-GRAPH-PADDING + CREATE-GRAPH-MIN-ZOOM + CREATE-GRAPH-MAX-ZOOM + CREATE-GRAPH-DEFAULT-ZOOM + CREATE-GRAPH-ZOOM-FACTOR + TIMELINE-ROW-HEIGHT + TIMELINE-MIN-TICK-PADDING + HEADER-PADDING + DEFAULT-TIMELINE-WIDTH + HEADER-HEIGHT + TOOLTIP-MARGIN) + +(define DEF-WINDOW-WIDTH 1500) +(define DEF-WINDOW-HEIGHT 1000) +(define RT-THREAD-ID 0) +(define MIN-SEG-WIDTH 10) +(define STROKE-WIDTH 2) +(define MIN-SEG-INNER-WIDTH (- MIN-SEG-WIDTH STROKE-WIDTH)) +;Default time interval (in MS) between ticks on the timeline +(define DEFAULT-TIME-INTERVAL (/ 1 10)) +(define TIMELINE-HEADER-OPACITY 0.6) +(define CONNECTION-LINE-HAT-THRESHOLD 20) +(define HAT-HEIGHT 9) +(define CREATE-GRAPH-NODE-DIAMETER 30) +(define CREATE-GRAPH-PADDING 5) +(define CREATE-GRAPH-MIN-ZOOM 1) +(define CREATE-GRAPH-MAX-ZOOM 5) +(define CREATE-GRAPH-DEFAULT-ZOOM 3) +(define CREATE-GRAPH-ZOOM-FACTOR .4) +(define TIMELINE-ROW-HEIGHT 100) +(define TIMELINE-MIN-TICK-PADDING 10) +(define HEADER-PADDING 5) +(define DEFAULT-TIMELINE-WIDTH 1000) +(define HEADER-HEIGHT 30) +(define TOOLTIP-MARGIN 5) \ No newline at end of file diff --git a/collects/racket/future/private/display.rkt b/collects/racket/future/private/display.rkt new file mode 100644 index 0000000000..21f75e6fea --- /dev/null +++ b/collects/racket/future/private/display.rkt @@ -0,0 +1,126 @@ +#lang racket/base +(provide get-event-color + get-event-forecolor + header-forecolor + header-backcolor + timeline-event-baseline-color + event-connection-line-color + event-target-future-line-color + timeline-tick-color + timeline-tick-bold-color + timeline-tick-label-backcolor + timeline-tick-label-forecolor + timeline-baseline-color + timeline-frame-color + timeline-frame-bg-color + timeline-event-strokecolor + hover-tickline-color + create-graph-node-backcolor + create-graph-node-strokecolor + create-graph-node-forecolor + create-graph-edge-color + create-graph-block-node-forecolor + create-graph-sync-node-forecolor + get-time-string + (struct-out viewable-region) + viewable-region-x-extent + viewable-region-y-extent + in-viewable-region + in-viewable-region-horiz + scale-viewable-region + between) + +(struct viewable-region (x y width height) #:transparent) + +;;viewable-region-x-extent : viewable-region -> uint +(define (viewable-region-x-extent vregion) + (+ (viewable-region-x vregion) (viewable-region-width vregion))) + +;;viewable-region-y-extent : viewable-region -> uint +(define (viewable-region-y-extent vregion) + (+ (viewable-region-y vregion) (viewable-region-height vregion))) + +(define (scale-viewable-region vreg factor) + (define (scale n) (* n factor)) + (struct-copy viewable-region vreg + [width (scale (viewable-region-width vreg))] + [height (scale (viewable-region-height vreg))])) + + +;;between : uint uint uint -> bool +(define (between x start end) + (and (>= x start) (<= x end))) + +;;in-viewable-region : viewable-region uint -> bool +(define (in-viewable-region-horiz vregion x) + (between x (viewable-region-x vregion) (viewable-region-x-extent vregion))) + +;;in-viewable-region : viewable-region segment -> bool +(define (in-viewable-region vregion x y w h) + (define-values (start-x start-y end-x end-y) + (values (viewable-region-x vregion) + (viewable-region-y vregion) + (viewable-region-x-extent vregion) + (viewable-region-y-extent vregion))) + (define-values (x-end y-end) + (values (+ x w) + (+ y h))) + (and (or (between x start-x end-x) + (between x-end start-x end-x) + (between start-x x x-end) + (between end-y y y-end)) + (or (between y start-y end-y) + (between y-end start-y end-y) + (between start-y y y-end) + (between end-y y y-end)))) + +;;get-event-color : symbol -> string +(define (get-event-color type) + (case type + [(create) "blue"] + [(start-work start-0-work touch-resume) "green"] + [(block touch) "red"] + [(sync) "orange"] + [(touch-pause) "blue"] + [(result abort suspend) "white"] + [(complete end-work) "white"] + [else "black"])) + +;;get-event-forecolor : symbol -> string +(define (get-event-forecolor type) + (case type + [(block) "white"] + [else "black"])) + +(define (header-forecolor) "white") +(define (header-backcolor) "slategray") +(define (timeline-event-baseline-color) "gray") +(define (event-connection-line-color) "orchid") +(define (event-target-future-line-color) "orange") +(define (creation-line-color) "green") +(define (touch-line-color) "red") +(define (timeline-tick-color) "gray") +(define (timeline-tick-bold-color) "darkgray") +(define (timeline-tick-label-backcolor) "darkgray") +(define (timeline-tick-label-forecolor) "white") +(define (timeline-baseline-color) "darkgray") +(define (timeline-frame-color) "gray") +(define (timeline-frame-bg-color) "white") +(define (timeline-event-strokecolor) "darkgray") +(define (hover-tickline-color) "darkgray") +(define (create-graph-node-forecolor) "white") +(define (create-graph-node-backcolor) "steelblue") +(define (create-graph-node-strokecolor) "darkgray") +(define (create-graph-edge-color) "black") +(define (create-graph-block-node-forecolor) "white") +(define (create-graph-sync-node-forecolor) "white") + +(define (get-time-string time) + (if (or (= 0.0 time) (> time 0.1)) + (format "~a ms" time) + (format "~a μs" (* 1000 time)))) + + + + + diff --git a/collects/racket/future/private/drawing-helpers.rkt b/collects/racket/future/private/drawing-helpers.rkt new file mode 100644 index 0000000000..1005705522 --- /dev/null +++ b/collects/racket/future/private/drawing-helpers.rkt @@ -0,0 +1,109 @@ +#lang racket/base +(require slideshow/pict + "display.rkt" + "constants.rkt") +(provide opacity-layer + circle-pict + rect-pict + text-pict + text-block-pict + draw-line-onto + make-stand-out + at + draw-stack-onto) + +;;opacity-layer : float uint uint -> pict +(define (opacity-layer alpha w h) + (cellophane (colorize (filled-rectangle w h) + "white") + 0.6)) + +;;circle-pict : string string uint [uint] -> pict +(define (circle-pict color stroke-color width #:stroke-width [stroke-width 1]) + (pin-over (colorize (filled-ellipse width + width) + stroke-color) + (* stroke-width 2) + (* stroke-width 2) + (colorize (filled-ellipse (- width (* stroke-width 4)) + (- width (* stroke-width 4))) + color))) + +;;rect-pict : string string uint uint [uint] -> pict +(define (rect-pict color stroke-color width height #:stroke-width [stroke-width 1]) + (pin-over (colorize (filled-rectangle width height) + stroke-color) + (* stroke-width 2) + (* stroke-width 2) + (colorize (filled-rectangle (- width (* stroke-width 4)) + (- height (* stroke-width 4))) + color))) + +;;text-pict : string [string] -> pict +(define (text-pict t #:color [color "black"]) + (colorize (text t) color)) + +;;text-block-pict : string [string] [string] [uint] [float] [uint] [uint] -> pict +(define (text-block-pict t #:backcolor [backcolor "white"] + #:forecolor [forecolor "black"] + #:padding [padding 10] + #:opacity [opacity 1.0] + #:width [width 0] + #:height [height 0]) + (let* ([textp (colorize (text t) forecolor)] + [padx2 (* padding 2)] + [text-cont (pin-over (blank (+ (pict-width textp) padx2) + (+ (pict-height textp) padx2)) + padding + padding + textp)] + [bg (cellophane (colorize (filled-rectangle (max width (pict-width text-cont)) + (max height (pict-height text-cont))) + backcolor) + opacity)]) + (lc-superimpose bg text-cont))) + +;;draw-line-onto : pict uint uint uint uint string -> pict +(define (draw-line-onto base + startx + starty + endx + endy + color + #:width [width 1] + #:with-arrow [with-arrow #f] + #:arrow-sz [arrow-sz 10] + #:style [style 'solid]) + (let ([dx (- endx startx)] + [dy (- endy starty)] + [line-f (if with-arrow pip-arrow-line pip-line)]) + (pin-over base + startx + starty + (linewidth width + (linestyle style + (colorize (line-f dx + dy + arrow-sz) + color)))))) + +;;make-stand-out : pict -> pict +(define (make-stand-out pict) + (scale pict 2)) + +(struct draw-at (x y p) #:transparent) + +;;at : uint uint pict -> draw-at +(define (at x y p) + (draw-at x y p)) + +;;draw-stack-onto : pict (listof pict) -> pict +(define (draw-stack-onto base . picts) + (for/fold ([p base]) ([cur-p (in-list picts)]) + (cond + [(pict? cur-p) (pin-over p 0 0 cur-p)] + [(draw-at? cur-p) (pin-over p + (draw-at-x cur-p) + (draw-at-y cur-p) + (draw-at-p cur-p))] + [else (error 'draw-onto "Invalid argument in 'picts' list.")]))) \ No newline at end of file diff --git a/collects/racket/future/private/graph-drawing.rkt b/collects/racket/future/private/graph-drawing.rkt new file mode 100644 index 0000000000..31f9d8de71 --- /dev/null +++ b/collects/racket/future/private/graph-drawing.rkt @@ -0,0 +1,440 @@ +#lang racket +(require rackunit) +(provide (struct-out point) + (struct-out node) + (struct-out drawable-node) + (struct-out graph-layout) + draw-tree + drawable-node-center) + +(define DEFAULT-WIDTH 10) +(define PADDING 5) +(define-struct/contract point ([x integer?] [y integer?]) #:transparent) +(struct node (data children)) +(struct graph-layout (width height nodes) #:transparent) +(struct drawable-node (node x y width depth children children-xextent children-yextent) #:transparent) + +(define (int x) + (floor (exact->inexact x))) + +;;Gets the center point of a node circle. +;;drawable-node-center : node -> point +(define (drawable-node-center dnode) + (point (int (+ (drawable-node-x dnode) (/ (drawable-node-width dnode) 2))) + (int (+ (drawable-node-y dnode) (/ (drawable-node-width dnode) 2))))) + +; +; ;; ;; +; ;;; ; ; ; ; +; ; ;; ; ; ; +; ; ; ;;;;;;; ;;;; ;; ;;; ;;; ; ;;;; ;; ;; ;;; ; +; ; ; ; ; ;; ; ; ;; ; ; ;;; ; ;; +; ;;;; ; ; ; ; ; ; ; ; ; ; +; ; ; ;;;;;; ; ; ; ; ;;;;;; ; ; ; +; ; ; ; ; ; ; ; ; ; ; ; ; ; +; ; ; ; ; ; ; ; ; ; ; ; ; ; ; +; ;; ; ; ; ;; ; ; ; ;; ; ;; ; ; ;; +; ; ;;; ;;;; ;;;; ;; ;;; ;;; ;;; ;; ;;;; ;; ;;;;;; ;;; ;; +; + +;;draw-tree/standard : node uint uint uint uint uint -> drawable-node +(define (draw-tree/standard parent x y depth node-width padding) + (if (empty? (node-children parent)) + (drawable-node parent + (+ padding x) + (+ padding y) + node-width + depth + '() + (+ padding x node-width) + (+ padding y node-width)) + (let ([child-y (+ y node-width)] + [children (node-children parent)] + [parenty (+ y padding)]) + (if (= 1 (length children)) ;Align parent and child vertically + (let ([child (draw-tree/standard (first children) + x + (+ parenty node-width) + (add1 depth) + node-width + padding)]) + (drawable-node parent + (drawable-node-x child) + parenty + node-width + depth + (list child) + (drawable-node-children-xextent child) + (drawable-node-children-yextent child))) + (let-values ([(x-extent + y-extent + children) + (for/fold ([xacc x] [yacc y] [chn '()]) + ([child (in-list children)]) + (let ([dchild (draw-tree/standard child + xacc + (+ parenty node-width) + (add1 depth) + node-width + padding)]) + (values (drawable-node-children-xextent dchild) + (drawable-node-children-yextent dchild) + (cons dchild chn))))]) + (let* ([chn (reverse children)] + [xmin (drawable-node-x (first chn))] + [xmax (drawable-node-x (last chn))]) + (drawable-node parent + (+ xmin (/ (- xmax xmin) 2)) + parenty + node-width + depth + chn + x-extent + (+ y-extent node-width)))))))) + + ;; ; ;;; +; ;;;;;; ; ; +; ; ;; ; ; +; ; ; ;;;; ;;; ; ;;; ;;;; ; +; ; ; ; ; ; ;; ; ; ; ; +; ; ; ; ; ; ; ; ; +; ;;;;; ;;;;;; ; ; ; ;;;;;; ; +; ; ; ; ; ; ; ; ; ; ; +; ; ; ; ; ; ; ; ; ; ; +; ; ; ; ;; ; ;; ; ; ;; ; +; ;;; ;; ;;;; ;; ;;; ;; ;;;;;; ;;;; ;; ;;;;;; +; + +;(r * cos(deg), r * sin(deg)) = point on circle given angle and radius r. + +(struct attributed-node (node type num-leaves depth children)) +(define (leaf? anode) + (equal? (attributed-node-type anode) 'leaf)) + +;;build-attr-tree : node uint -> attributed-node +(define (build-attr-tree parent depth) + (if (empty? (node-children parent)) + (attributed-node parent 'leaf 0 depth '()) + (let-values ([(leaves achn) + (for/fold ([l 0] [achildren '()]) ([child (in-list (node-children parent))]) + (let ([anode (build-attr-tree child (add1 depth))]) + (if (leaf? anode) + (values (add1 l) (cons anode achildren)) + (values (+ l (attributed-node-num-leaves anode)) (cons anode achildren)))))]) + (attributed-node parent + 'interior + leaves + depth + achn)))) + + +;(struct drawable-node (node x y width depth children children-xextent children-yextent) #:transparent) +;;draw-tree/radial : node uint (uint -> uint) uint -> drawable-node +(define (draw-tree/radial root node-width Bv p depth) + (let* ([atree (build-attr-tree root 0)] + #;[angle-incr (/ Bv (length (attributed-node-children root)))]) + (for/fold ([angle 0] [chn '()]) ([achild (in-list (attributed-node-children atree))]) + (let* ([Bu (/ (* (attributed-node-num-leaves achild) Bv) + (attributed-node-num-leaves atree))] + [pa (+ angle + (/ Bu 2))] + [x (* (p depth) (cos pa))] + [y (* (p depth) (sin pa))]) + (values (+ angle Bu) + (cons (drawable-node (attributed-node-node achild) + x + y + node-width + depth + '() + 0 + 0) chn)))))) + +;;tree-layout/private : drawable-node uint uint (listof drawable-node) -> (values uint uint (listof drawable-node)) +(define (tree-layout/private parent xextent yextent nodes) + (if (empty? (drawable-node-children parent)) + (values (max (+ (drawable-node-x parent) (drawable-node-width parent)) xextent) + (max (+ (drawable-node-y parent) (drawable-node-width parent)) yextent) + (cons parent nodes)) + (for/fold ([x xextent] [y yextent] [ns (cons parent nodes)]) ([child (in-list (drawable-node-children parent))]) + (tree-layout/private child x y (cons child ns))))) + +;;calc-tree-layout : drawable-node uint uint -> graph-layout +(define (calc-tree-layout root node-width padding) + (define-values (w h nodes) (tree-layout/private root 0 0 '())) + (graph-layout w + h + nodes)) + +;;draw-tree : node [symbol] [uint] [uint] [uint] -> tree-layout +(define (draw-tree root + #:style [style 'standard] + #:node-width [node-width DEFAULT-WIDTH] + #:padding [padding PADDING] + #:zoom [zoom-level 1]) + (let* ([scaled-node-w (* node-width zoom-level)] + [scaled-padding (* padding zoom-level)] + [layout + (case style + [(standard) (calc-tree-layout (draw-tree/standard root + 0 + 0 + 0 + scaled-node-w + scaled-padding) + scaled-node-w + scaled-padding)] + [(radial) (calc-tree-layout (draw-tree/radial root + (λ (i) (* i 50))) + scaled-node-w + scaled-padding)] + [(hv) 0] + [else + (error 'draw-tree "Invalid tree drawing style.")])]) + (graph-layout (+ (graph-layout-width layout) scaled-padding) + (+ (graph-layout-height layout) scaled-padding) + (graph-layout-nodes layout)))) + + +;Tests +(let* ([nodea (drawable-node (node 'a '()) 5 5 10 0 0 '() 10)] + [center (drawable-node-center nodea)]) + (check-equal? (point-x center) 10.0) + (check-equal? (point-y center) 10.0)) + + +(define test-padding 5) +(define test-width 10) + +(define (tree root-data . children) + (node root-data children)) + +(define (get-node data layout) + (first (filter (λ (dn) (equal? (node-data (drawable-node-node dn)) data)) (graph-layout-nodes layout)))) + +#| + a + | + b +|# +(define tree0 (tree 'a (tree 'b))) +(let* ([layout (draw-tree tree0 #:node-width test-width #:padding test-padding)] + [dnode-a (get-node 'a layout)] + [dnode-b (get-node 'b layout)]) + (check-equal? (graph-layout-width layout) (+ (* test-padding 2) test-width)) + (check-equal? (graph-layout-height layout) (+ (* test-padding 3) (* test-width 2))) + (check-equal? (drawable-node-x dnode-a) test-padding) + (check-equal? (drawable-node-y dnode-a) test-padding) + (check-equal? (drawable-node-x dnode-b) test-padding) + (check-equal? (drawable-node-y dnode-b) (+ test-padding test-width test-padding))) +(let ([atree (build-attr-tree tree0 0)]) + (check-equal? (attributed-node-num-leaves atree) 1)) + +#| + a + / \ + b c +|# +(define tree1 (tree 'a + (tree 'b) + (tree 'c))) +(define layout (draw-tree tree1 #:node-width test-width #:padding test-padding)) +(for ([dnode (in-list (graph-layout-nodes layout))]) + (check-equal? (drawable-node-width dnode) test-width)) +(define dnode-a (get-node 'a layout)) +(define dnode-b (get-node 'b layout)) +(define dnode-c (get-node 'c layout)) + +(define slot-one-pos (+ test-padding test-width test-padding)) +(define square-sz (+ (* test-padding 3) (* test-width 2))) +(check-equal? (graph-layout-width layout) square-sz) +(check-equal? (graph-layout-height layout) square-sz) +(check-equal? (drawable-node-x dnode-b) test-padding) +(check-equal? (drawable-node-y dnode-b) slot-one-pos) +(check-equal? (drawable-node-x dnode-c) slot-one-pos) +(check-equal? (drawable-node-y dnode-c) slot-one-pos) +(check-equal? (drawable-node-x dnode-a) (/ 25 2)) +(check-equal? (drawable-node-y dnode-a) test-padding) +(check-equal? (length (drawable-node-children dnode-a)) 2) +(let ([atree (build-attr-tree tree1 0)]) + (check-equal? (attributed-node-num-leaves atree) 2)) + +#| + a + / \ + b d + | / \ + c e f + | + g +|# +(define tree2 (tree 'a + (tree 'b + (tree 'c)) + (tree 'd + (tree 'e) + (tree 'f + (tree 'g))))) +(let* ([layout (draw-tree tree2 #:node-width test-width #:padding test-padding)] + [nodes (graph-layout-nodes layout)] + [dnode-a (get-node 'a layout)] + [dnode-b (get-node 'b layout)] + [dnode-c (get-node 'c layout)] + [dnode-d (get-node 'd layout)] + [dnode-e (get-node 'e layout)] + [dnode-f (get-node 'f layout)] + [dnode-g (get-node 'g layout)]) + (check-equal? (node-data (drawable-node-node dnode-a)) 'a) + (check-equal? (node-data (drawable-node-node dnode-b)) 'b) + (check-equal? (node-data (drawable-node-node dnode-c)) 'c) + (check-equal? (node-data (drawable-node-node dnode-d)) 'd) + (check-equal? (node-data (drawable-node-node dnode-e)) 'e) + (check-equal? (node-data (drawable-node-node dnode-f)) 'f) + (check-equal? (node-data (drawable-node-node dnode-g)) 'g) + (check-equal? (graph-layout-width layout) 50) + (check-equal? (graph-layout-height layout) 65) + (check-equal? (drawable-node-x dnode-a) (/ 65 4)) + (check-equal? (drawable-node-y dnode-a) test-padding) + (check-equal? (drawable-node-x dnode-b) test-padding) + (check-equal? (drawable-node-y dnode-b) (+ (* 2 test-padding) test-width)) + (check-equal? (drawable-node-x dnode-c) test-padding) + (check-equal? (drawable-node-y dnode-c) (+ (drawable-node-y dnode-b) test-width test-padding)) + (check-equal? (drawable-node-x dnode-e) (+ (* 2 test-padding) test-width)) + (check-equal? (drawable-node-y dnode-e) (+ (drawable-node-y dnode-d) test-width test-padding)) + (check-equal? (drawable-node-x dnode-f) (+ (drawable-node-x dnode-e) test-width test-padding)) + (check-equal? (drawable-node-y dnode-f) (drawable-node-y dnode-e)) + (check-equal? (drawable-node-x dnode-g) (drawable-node-x dnode-f)) + (check-equal? (drawable-node-y dnode-g) (+ (drawable-node-y dnode-f) test-width test-padding))) +(let ([atree (build-attr-tree tree2 0)]) + (check-equal? (attributed-node-num-leaves atree) 3)) + +#| + a + /|\ + b c e + | + d +|# +(define tree3 (tree 'a + (tree 'b) + (tree 'c + (tree 'd)) + (tree 'e))) +(let* ([layout (draw-tree tree3 #:node-width test-width #:padding test-padding)] + [nodes (graph-layout-nodes layout)] + [dnode-a (get-node 'a layout)] + [dnode-b (get-node 'b layout)] + [dnode-c (get-node 'c layout)] + [dnode-d (get-node 'd layout)] + [dnode-e (get-node 'e layout)]) + (check-equal? (graph-layout-width layout) 50) + (check-equal? (graph-layout-height layout) 50) + (check-equal? (drawable-node-x dnode-a) 20) + (check-equal? (drawable-node-y dnode-a) 5) + (check-equal? (drawable-node-x dnode-b) test-padding) + (check-equal? (drawable-node-y dnode-b) (+ (* 2 test-padding) test-width)) + (check-equal? (drawable-node-x dnode-c) (+ (* 2 test-padding) test-width)) + (check-equal? (drawable-node-y dnode-c) (drawable-node-y dnode-b)) + (check-equal? (drawable-node-x dnode-e) (+ (* 3 test-padding) (* 2 test-width))) + (check-equal? (drawable-node-y dnode-e) (drawable-node-y dnode-c)) + (check-equal? (drawable-node-x dnode-d) (drawable-node-x dnode-c)) + (check-equal? (drawable-node-y dnode-d) (+ (drawable-node-y dnode-c) test-padding test-width))) +(let ([atree (build-attr-tree tree3 0)]) + (check-equal? (attributed-node-num-leaves atree) 3)) + +#| + a + / | | \ + b c f g + / \ + d e +|# +(define tree4 (tree 'a + (tree 'b) + (tree 'c + (tree 'd) + (tree 'e)) + (tree 'f) + (tree 'g))) +(let* ([layout (draw-tree tree4 #:node-width test-width #:padding test-padding)] + [nodes (graph-layout-nodes layout)] + [dnode-a (get-node 'a layout)] + [dnode-b (get-node 'b layout)] + [dnode-c (get-node 'c layout)] + [dnode-d (get-node 'd layout)] + [dnode-e (get-node 'e layout)] + [dnode-f (get-node 'f layout)] + [dnode-g (get-node 'g layout)]) + (check-equal? (graph-layout-width layout) 80) + (check-equal? (graph-layout-height layout) 50) + (check-equal? (drawable-node-x dnode-b) test-padding) + (check-equal? (drawable-node-y dnode-b) (+ (drawable-node-y dnode-a) test-width test-padding)) + (check-equal? (drawable-node-y dnode-c) (drawable-node-y dnode-b)) + (check-equal? (drawable-node-x dnode-d) (+ (drawable-node-x dnode-b) test-width test-padding)) + (check-equal? (drawable-node-y dnode-d) (+ (drawable-node-y dnode-c) test-width test-padding)) + (check-equal? (drawable-node-x dnode-e) (+ (drawable-node-x dnode-d) test-width test-padding)) + (check-equal? (drawable-node-y dnode-e) (drawable-node-y dnode-d)) + (check-equal? (drawable-node-x dnode-f) (+ (drawable-node-x dnode-e) test-width test-padding)) + (check-equal? (drawable-node-y dnode-f) (drawable-node-y dnode-c)) + (check-equal? (drawable-node-x dnode-g) (+ (drawable-node-x dnode-f) test-width test-padding))) +(let ([atree (build-attr-tree tree4 0)]) + (check-equal? (attributed-node-num-leaves atree) 5)) + +#| +Layered-tree-draw example from Di Battista + a + / \ + b g + | / \ + c h k + | / \ + d i j + / \ + e f +|# +(define tree5 (tree 'a + (tree 'b + (tree 'c + (tree 'd + (tree 'e) + (tree 'f)))) + (tree 'g + (tree 'h + (tree 'i) + (tree 'j)) + (tree 'k)))) +(let* ([layout (draw-tree tree5 #:node-width test-width #:padding test-padding)] + [nodes (graph-layout-nodes layout)] + [dnode-a (get-node 'a layout)] + [dnode-b (get-node 'b layout)] + [dnode-c (get-node 'c layout)] + [dnode-d (get-node 'd layout)] + [dnode-e (get-node 'e layout)] + [dnode-f (get-node 'f layout)] + [dnode-g (get-node 'g layout)] + [dnode-h (get-node 'h layout)] + [dnode-i (get-node 'i layout)] + [dnode-j (get-node 'j layout)] + [dnode-k (get-node 'k layout)]) + (check-equal? (graph-layout-width layout) 80) + (check-equal? (graph-layout-height layout) 80) + (check-equal? (drawable-node-x dnode-e) test-padding) + (check-equal? (drawable-node-y dnode-e) 65) + (check-equal? (drawable-node-x dnode-f) (+ (drawable-node-x dnode-e) test-width test-padding)) + (check-equal? (drawable-node-x dnode-i) (+ (drawable-node-x dnode-f) test-width test-padding)) + (check-equal? (drawable-node-x dnode-j) (+ (drawable-node-x dnode-i) test-width test-padding)) + (check-equal? (drawable-node-x dnode-k) (+ (drawable-node-x dnode-j) test-width test-padding))) +(let ([atree (build-attr-tree tree5 0)]) + (check-equal? (attributed-node-num-leaves atree) 5)) + + + + + + + + + + + + \ No newline at end of file diff --git a/collects/racket/future/private/gui-helpers.rkt b/collects/racket/future/private/gui-helpers.rkt new file mode 100644 index 0000000000..0214ed1f7e --- /dev/null +++ b/collects/racket/future/private/gui-helpers.rkt @@ -0,0 +1,213 @@ +#lang racket/gui +(require framework + slideshow/pict + "display.rkt" + "constants.rkt") +(provide pict-canvas% + label + mt-label + bold-label + mt-bold-label + section-header + (struct-out event-target) + make-listener-table + add-receiver + post-event) + +(define pict-canvas% + (class canvas% + (init redraw-on-resize pict-builder hover-handler click-handler overlay-builder) + (inherit get-dc get-client-size refresh get-view-start) + (define bp pict-builder) ;Builds the main pict for the canvas + (define mh hover-handler) ;Mouse hover handler + (define ob overlay-builder) ;Hover overlay pict builder + (define ch click-handler) ;Mouse click handler + (define draw-on-resize redraw-on-resize) + (define do-logging #f) + (define redraw-overlay #f) ;Whether we should redraw the overlay pict in the canvas + (define redo-bitmap-on-paint #f) + + (define/public (set-redo-bitmap-on-paint! v) + (set! redo-bitmap-on-paint v)) + + (define/public (set-do-logging! v) + (set! do-logging v)) + + ;;set-build-pict! : (viewable-region -> pict) -> void + (define/public (set-build-pict! f) + (set! bp f)) + + ;;set-mouse-handler! : (uint uint -> segment) -> void + (define/public (set-mouse-handler! f) + (set! mh f)) + + ;;set-overlay-builder! : (viewable-region -> pict) -> void + (define/public (set-overlay-builder! f) + (set! ob f)) + + ;;set-click-handler! : (uint uint -> segment) -> void + (define/public (set-click-handler! f) + (set! ch f)) + + ;;set-redraw-overlay! : bool -> void + (define/public (set-redraw-overlay! b) + (set! redraw-overlay b)) + + (define the-drawer #f) + (define img-width 0) + (define bm #f) + (define overlay-pict #f) + + (define/private (get-viewable-region) + (define-values (x y) (get-view-start)) + (define-values (w h) (get-client-size)) + (viewable-region x y w h)) + + (define/private (overlay-drawer dc vregion) + (when ob + (define p (ob vregion)) + (unless (or (not p) (void? p)) + (draw-pict p + dc + (viewable-region-x vregion) + (viewable-region-y vregion))))) + + (define/private (redo-bitmap vregion) + (when bp + (define p (bp vregion)) + (set! bm (pict->bitmap p)))) + + (define/public (redraw-everything) + (redo-bitmap (get-viewable-region)) + (refresh)) + + (define/override (on-size width height) + (when (or draw-on-resize + (not bm)) + (set! bm #f) + (refresh)) + (set! redraw-overlay #t)) + + (define/override (on-paint) + (define vregion (get-viewable-region)) + (when (or redo-bitmap-on-paint (not bm)) + (redo-bitmap vregion)) + (when bm + (let ([dc (get-dc)]) + (send dc draw-bitmap + bm + (viewable-region-x vregion) + (viewable-region-y vregion)) + (overlay-drawer dc vregion)))) + + (define/override (on-event event) + (define vregion (get-viewable-region)) + (define x (+ (viewable-region-x vregion) (send event get-x))) + (define y (+ (viewable-region-y vregion) (send event get-y))) + (case (send event get-event-type) + [(motion) + (when mh (mh x y vregion))] + [(left-up) + (when ch (ch x y vregion))]) + (when redraw-overlay + (refresh))) + + (super-new) + (send (get-dc) set-smoothing 'aligned))) + +(define bold-system-font + (send the-font-list find-or-create-font + (send normal-control-font get-point-size) + (send normal-control-font get-family) + (send normal-control-font get-style) + 'bold)) + +(define (label p str) + (new message% [parent p] + [label str] + [stretchable-width #t])) + +(define (mt-label p) + (label p "")) + +(define (bold-label p str) + (new message% [parent p] + [label str] + [font bold-system-font] + [stretchable-width #t])) + +(define (mt-bold-label p) + (bold-label p "")) + +(define (section-header par name orientation) + (let* ([text-pict (colorize (text name) (header-forecolor))] + [text-container (pin-over (colorize (rectangle (+ 10 (pict-width text-pict)) + (+ 10 (pict-height text-pict))) + (header-backcolor)) + 5 + 5 + text-pict)] + [c (case orientation + [(horizontal) + (let ([canv (new pict-canvas% + [parent par] + [redraw-on-resize #t] + [pict-builder (λ (vregion) + (lc-superimpose (colorize (filled-rectangle (viewable-region-width vregion) + HEADER-HEIGHT) + (header-backcolor)) + text-container))] + [hover-handler #f] + [click-handler #f] + [overlay-builder #f] + [min-height HEADER-HEIGHT] + [stretchable-width #t] + [stretchable-height #f])]) + canv)] + [(vertical) + (let ([canv (new pict-canvas% + [parent par] + [redraw-on-resize #t] + [pict-builder (λ (vregion) + (rotate (lc-superimpose (colorize (filled-rectangle (viewable-region-height vregion) + HEADER-HEIGHT) + (header-backcolor)) + text-container) + -1.57079633))] + [hover-handler #f] + [click-handler #f] + [overlay-builder #f] + [min-width HEADER-HEIGHT] + [stretchable-width #f] + [stretchable-height #t])]) + canv)])]) + c)) + +;Events +;receiver : any +;handler : (any -> void) +(struct event-target (receiver handler) #:transparent) + +(define (make-listener-table) (make-hash)) + +(define (add-receiver table evt-name object handler) + (hash-update! table + evt-name + (λ (old) + (cons (event-target object handler) old)) + (list (event-target object handler)))) + +(define (post-event table name sender arg) + (let ([targets (hash-ref table name)]) + (for ([target (in-list targets)]) + (let ([receiver (event-target-receiver target)] + [handler (event-target-handler target)]) + (unless (eq? receiver sender) + (handler arg)))))) + + + + + + + \ No newline at end of file diff --git a/collects/racket/future/private/visualizer-data.rkt b/collects/racket/future/private/visualizer-data.rkt new file mode 100644 index 0000000000..39168ca3da --- /dev/null +++ b/collects/racket/future/private/visualizer-data.rkt @@ -0,0 +1,365 @@ +#lang racket/base +(require racket/bool + racket/list + racket/contract + racket/future + racket/set + "constants.rkt" + "graph-drawing.rkt") + +(provide (contract-out [start-performance-tracking! (-> void?)]) + (struct-out future-event) + (struct-out indexed-fevent) + (struct-out trace) + (struct-out process-timeline) + (struct-out future-timeline) + (struct-out event) + (struct-out rtcall-info) + raw-log-output + organize-output + build-trace + event-has-duration? + final-event? + relative-time) + +;Log message receiver +(define recv #f) + +;;start-performance-tracking! -> void +(define (start-performance-tracking!) + (when (not recv) + (set! recv (make-log-receiver (current-logger) 'debug)))) + +(define-struct future-event (future-id process-id what time prim-name user-data) + #:prefab) + +;Contains an index and a future-event, +;so we can preserve the order in which future-events +;were logged. +;Many future-events can be logged at what appears to be the same +;time, apparently because the time values don't have great enough precision +;to separate events which temporally occur close together. +(struct indexed-fevent (index fevent) #:transparent) + +;The whole trace, with a start/end time and list of process timelines +(struct trace (start-time + end-time + proc-timelines + future-timelines + all-events + real-time ;TODO: What is this + num-futures ;TODO: (length future-timelines) + num-blocks + num-syncs + blocked-futures + avg-syncs-per-future + block-counts ;prim name --o--> number of blocks + sync-counts ;op name --o--> number of syncs + future-rtcalls ;fid --o--> rtcall-info + creation-tree)) + +(struct rtcall-info (fid + block-hash ; prim name --o--> number of blocks + sync-hash) ; op name --o--> number of syncs + #:transparent) + +;The timeline of events for a specific process +(struct timeline (id + start + end + events)) +;(struct process-timeline timeline (proc-index)) +(struct process-timeline (proc-id + proc-index ;Why do we need this + start-time + end-time + events)) +;(struct future-timeline timeline ()) +(struct future-timeline (future-id + start-time + end-time + events)) + +;A block of time (e.g. a process executing a portion of a future thunk). +(struct event (index + start-time + end-time + proc-id + proc-index ;TODO: why here? + future-id + user-data + type + prim-name + timeline-position ;TODO: what is this + [prev-proc-event #:mutable] + [next-proc-event #:mutable] + [prev-future-event #:mutable] + [next-future-event #:mutable] + [next-targ-future-event #:mutable] + [prev-targ-future-event #:mutable] + [segment #:mutable]) #:transparent) + +;;event-has-duration? : event -> bool +(define (event-has-duration? evt) + (case (event-type evt) + [(start-work start-0-work) #t] + [else #f])) + +;;final-event? : event -> bool +(define (final-event? evt) + (case (event-timeline-position evt) + [(end singleton) #t] + [else #f])) + +(define (get-log-events) + (let ([info (sync/timeout 0 recv)]) + (if info + (let ([v (vector-ref info 2)]) + (cons v (get-log-events))) + '()))) + +;;get-relative-start-time : trace float -> float +(define (relative-time trace abs-time) + (- abs-time (trace-start-time trace))) + +;Gets log output as a straight list, ordered according to when the +;message was logged +;;raw-log-output : uint -> (listof indexed-fevent) +(define (raw-log-output index) + (let ([info (sync/timeout 0 recv)]) + (if info + (let ([v (vector-ref info 2)]) + (if (future-event? v) + (cons (indexed-fevent index v) (raw-log-output (add1 index))) + (raw-log-output index))) + '()))) + +(define (print-blocks raw-output) + (for ([fe (in-list raw-output)]) + (when (equal? (future-event-what fe) 'block) + (printf "~a\n" (future-event-prim-name fe))))) + +;Produces a vector of vectors, where each inner vector contains +;all the log output messages for a specific process +;;organize-output : (listof indexed-fevent) -> (vectorof (vectorof future-event)) +(define (organize-output raw-log-output) + ;TODO: Try using for/set here, does calling code depend on ordering + #;(define unique-proc-ids (for/set ([ie (in-list raw-log-output)]) + (future-event-process-id (indexed-fevent-fevent ie)))) + (let ([unique-proc-ids (sort (for/fold ([ids '()]) ([ie (in-list raw-log-output)]) + (let* ([evt (indexed-fevent-fevent ie)] + [procid (future-event-process-id evt)]) + (if (member procid ids) + ids + (cons procid ids)))) + <)]) + (for/vector ([procid (in-list unique-proc-ids)]) + (for/vector ([e (in-list raw-log-output)] + #:when (eq? procid (future-event-process-id (indexed-fevent-fevent e)))) + e)))) + +;;build-trace : (listof indexed-fevent) -> trace +(define (build-trace log-output) + (define data (organize-output log-output)) + (define-values (start-time end-time unique-fids nblocks nsyncs) + (for/fold ([start-time #f] + [end-time #f] + [unique-fids (set)] + [nblocks 0] + [nsyncs 0]) ([ie (in-list log-output)]) + (let* ([evt (indexed-fevent-fevent ie)] + [fid (future-event-future-id evt)] + [is-future-thread? (not (= (future-event-process-id evt) RT-THREAD-ID))]) + (values + (if start-time + (min start-time (future-event-time evt)) + (future-event-time evt)) + (if end-time + (max end-time (future-event-time evt)) + (future-event-time evt)) + (if fid + (set-add unique-fids fid) + unique-fids) + (if (and is-future-thread? + (case (future-event-what evt) + [(block touch) #t] + [else #f])) + (add1 nblocks) + nblocks) + (if (and is-future-thread? (symbol=? (future-event-what evt) 'sync)) + (add1 nsyncs) + nsyncs))))) + (define tls (for/list ([proc-log-vec (in-vector data)] + [i (in-naturals)]) + (let* ([fst-ie (vector-ref proc-log-vec 0)] + [fst-log-msg (indexed-fevent-fevent fst-ie)]) + (process-timeline (future-event-process-id fst-log-msg) + i + (future-event-time fst-log-msg) + (future-event-time (indexed-fevent-fevent + (vector-ref proc-log-vec + (sub1 (vector-length proc-log-vec))))) + (for/list ([ie (in-vector proc-log-vec)] + [j (in-naturals)]) + (let* ([evt (indexed-fevent-fevent ie)] + [start (future-event-time evt)] + [pos (cond + [(zero? j) (if (= j (sub1 (vector-length proc-log-vec))) + 'singleton + 'start)] + [(= j (sub1 (vector-length proc-log-vec))) 'end] + [else 'interior])]) + (event (indexed-fevent-index ie) + start + (if (or (equal? pos 'end) (equal? pos 'singleton)) + start + (future-event-time (indexed-fevent-fevent + (vector-ref proc-log-vec (add1 j))))) + (future-event-process-id evt) + i + (future-event-future-id evt) + (future-event-user-data evt) + (future-event-what evt) + (future-event-prim-name evt) + pos + #f + #f + #f + #f + #f + #f + #f))))))) + (define all-evts (sort (flatten (for/list ([tl (in-list tls)]) + (process-timeline-events tl))) + (λ (a b) + (< (event-index a) (event-index b))))) + (define ftls (let ([h (make-hash)]) + (for ([evt (in-list all-evts)]) + (let* ([fid (event-future-id evt)] + [existing (hash-ref h fid '())]) + (hash-set! h fid (cons evt existing)))) + h)) + (for ([fid (in-list (hash-keys ftls))]) + (hash-set! ftls fid (reverse (hash-ref ftls fid)))) + (define-values (block-hash sync-hash rt-hash) (build-rtcall-hash all-evts)) + (define tr (trace start-time + end-time + tls + ftls + all-evts + (- end-time start-time) ;real time + (set-count unique-fids) ;num-futures + nblocks ;num-blocks + nsyncs ;num-syncs + 0 + 0 + block-hash + sync-hash + rt-hash ;hash of fid -> rtcall-info + (build-creation-graph ftls))) + (connect-event-chains! tr) + (connect-target-fid-events! tr) + tr) + +;;build-rtcall-hash : (listof event) -> (values (blocking_prim --o--> count) (sync_prim --o--> count) (fid --o--> rtcall-info) +(define (build-rtcall-hash evts) + (define block-hash (make-hash)) + (define sync-hash (make-hash)) + (define rt-hash (make-hash)) + (for ([evt (in-list (filter (λ (e) (and (= (event-proc-id e) RT-THREAD-ID) + (or (equal? (event-type e) 'block) + (equal? (event-type e) 'sync)))) + evts))]) + ;(printf "event: ~a\n" evt) + (define isblock (case (event-type evt) + [(block) #t] + [else #f])) + (define ophash (if isblock block-hash sync-hash)) + (hash-update! ophash + (event-prim-name evt) + (λ (old) (add1 old)) + (λ () 1)) + (hash-update! rt-hash + (event-future-id evt) + (λ (old) + (let ([h (if isblock + (rtcall-info-block-hash old) + (rtcall-info-sync-hash old))]) + (hash-update! h + (event-prim-name evt) + (λ (o) (add1 o)) + (λ () 1))) + old) + (λ () + (let* ([ri (rtcall-info (event-future-id evt) (make-hash) (make-hash))] + [h (if isblock + (rtcall-info-block-hash ri) + (rtcall-info-sync-hash ri))]) + (hash-update! h + (event-prim-name evt) + (λ (o) (add1 o)) + (λ () 1)) + ri)))) + ; (printf "blocks: ~a\n syncs: ~a\n rts: ~a\n" block-hash sync-hash rt-hash) + (values block-hash sync-hash rt-hash)) + + + +;;connect-event-chains! : trace -> void +(define (connect-event-chains! trace) + (for ([tl (in-list (trace-proc-timelines trace))]) + (let loop ([evts (process-timeline-events tl)]) + (if (or (empty? evts) (empty? (cdr evts))) + void + (begin + (set-event-prev-proc-event! (first (cdr evts)) (car evts)) + (set-event-next-proc-event! (car evts) (first (cdr evts))) + (loop (cdr evts)))))) + (for ([fid (in-list (hash-keys (trace-future-timelines trace)))]) + (let ([events (hash-ref (trace-future-timelines trace) fid)]) + (let loop ([evts events]) + (if (or (empty? evts) (empty? (cdr evts))) + void + (begin + (set-event-prev-future-event! (first (cdr evts)) (car evts)) + (set-event-next-future-event! (car evts) (first (cdr evts))) + (loop (cdr evts)))))))) + +;;connect-target-fid-events! : trace -> void +(define (connect-target-fid-events! trace) + (let loop ([rest (trace-all-events trace)]) + (unless (empty? rest) + (let ([cur-evt (car rest)]) + (when (and (or (equal? (event-type cur-evt) 'create) + (equal? (event-type cur-evt) 'touch)) + (not (zero? (event-user-data cur-evt)))) + (let ([targ-evt (findf (λ (e) (and (event-future-id e) + (= (event-future-id e) + (event-user-data cur-evt)))) + (cdr rest))]) + (when targ-evt + (set-event-next-targ-future-event! cur-evt targ-evt) + (set-event-prev-targ-future-event! targ-evt cur-evt)))) + (loop (cdr rest)))))) + +;;creation-event : event -> bool +(define (creation-event? evt) + (equal? (event-type evt) 'create)) + +;;buid-creation-graph/private : (uint -o-> (listof future-event)) -> (listof node) +(define (build-creation-graph/private future-timelines evt) + (let* ([fid (event-user-data evt)] + [fevents (filter creation-event? (hash-ref future-timelines fid))]) + (for/list ([cevt (in-list fevents)]) + (node cevt + (build-creation-graph/private future-timelines cevt))))) + +;;build-creation-graph : (uint -o-> (listof future-event)) -> node +(define (build-creation-graph future-timelines) + (define roots (filter creation-event? + (hash-ref future-timelines #f))) + (define root-nodes (for/list ([root (in-list roots)]) + (node root + (build-creation-graph/private future-timelines root)))) + (node 'runtime-thread + root-nodes)) \ No newline at end of file diff --git a/collects/racket/future/private/visualizer-drawing.rkt b/collects/racket/future/private/visualizer-drawing.rkt new file mode 100644 index 0000000000..f73fae56c9 --- /dev/null +++ b/collects/racket/future/private/visualizer-drawing.rkt @@ -0,0 +1,831 @@ +#lang racket/base +(require racket/list + racket/class + racket/draw + slideshow/pict + data/interval-map + "visualizer-data.rkt" + "graph-drawing.rkt" + "drawing-helpers.rkt" + "display.rkt" + "constants.rkt") + +(provide seg-in-vregion + calc-segments + calc-ticks + calc-row-mid-y + find-seg-for-coords + segment-edge + segs-equal-or-later + build-timeline-pict + build-timeline-bmp-from-log + build-timeline-pict-from-log + build-timeline-overlay + build-timeline-with-overlay + build-timeline-bmp-with-overlay + build-creategraph-pict + graph-overlay-pict + (struct-out segment) + (struct-out frame-info) + (struct-out timeline-tick) + find-node-for-coords + find-fid-for-coords + first-seg-for-fid) + +;Represents a dot or square on the timeline +(struct segment (event + x + y + width + height + color + p + prev-future-seg + next-future-seg + prev-proc-seg + next-proc-seg + prev-targ-future-seg + next-targ-future-seg) #:transparent #:mutable) + + +;General information about the timeline image +(struct frame-info (adjusted-width + adjusted-height + row-height + modifier + timeline-ticks + process-line-coords) #:transparent) + +;Represents a vertical line depicting a specific time in the execution history +(struct timeline-tick (x + abs-time + rel-time) #:transparent) + +;;viewable-region-from-frame : frame-info -> viewable-region +(define (viewable-region-from-frame finfo) + (viewable-region 0 + 0 + (frame-info-adjusted-width finfo) + (frame-info-adjusted-height finfo))) + +;;seg-in-vregion : viewable-region segment -> bool +(define (seg-in-vregion vregion) + (λ (seg) + (in-viewable-region vregion + (segment-x seg) + (segment-y seg) + (segment-width seg) + (segment-height seg)))) + +;;calc-seg-x : event process-timeline trace uint float -> uint +(define (calc-seg-x evt tr modifier) + (floor (* (relative-time tr (event-start-time evt)) + modifier))) + +;;calc-seg-width : float event -> uint +(define (calc-seg-width modifier evt) + (case (event-type evt) + [(start-work start-0-work) (max MIN-SEG-WIDTH (* modifier (- (event-end-time evt) + (event-start-time evt))))] + [else MIN-SEG-WIDTH])) + +;Finds the segment for given x and y mouse coordinates +;;find-seg-for-coords : uint uint interval-map -> segment +(define (find-seg-for-coords x y index) + (let ([xmap (interval-map-ref index y #f)]) + (if xmap + (interval-map-ref xmap x #f) + #f))) + +;;find-fid-for-coords : uint uint (listof drawable-node) -> drawable-node +(define (find-node-for-coords x y nodes) + (define node-l (filter (λ (n) + (define n-x (drawable-node-x n)) + (define n-y (drawable-node-y n)) + (define n-w (drawable-node-width n)) + (and (n-x . < . x) + (n-y . < . y) + (x . < . (+ n-x n-w)) + (y . < . (+ n-y n-w)))) + (remove-duplicates (flatten nodes)))) + (cond + [(empty? node-l) + #f] + [(= 1 (length node-l)) + (car node-l)] + [else + (error 'find-node-for-coords "Multiple nodes found for coords: ~s ~s, ~s" x y node-l)])) + +;;find-fid-for-coords : x y ??(listof (listof nodes)) by depth?? viewable-region -> fid +(define (find-fid-for-coords x y nodes vregion) + (define n (find-node-for-coords x y nodes)) + (if n + (event-user-data (node-data (drawable-node-node n))) + #f)) + +;;first-seg-for-fid : future-id (listof segments) -> segment +(define (first-seg-for-fid fid segs) + (first + (sort + (filter (λ (s) (define seg-fid (event-future-id (segment-event s))) + (and seg-fid fid (= fid seg-fid))) segs) + < #:key (λ (s) (event-start-time (segment-event s)))))) + +;;calc-adjusted-width : uint trace -> uint +(define (calc-adjusted-width w tr) + (define baseModifier (/ w (- (trace-end-time tr) (trace-start-time tr)))) + (define max-x-extent (for*/fold ([x 0]) ([tl (in-list (trace-proc-timelines tr))] + [evt (in-list (process-timeline-events tl))]) + (max (+ (calc-seg-x evt tr baseModifier) + (calc-seg-width baseModifier evt)) + x))) + (- (- w (- max-x-extent w)) MIN-SEG-WIDTH)) + +;;calc-row-mid-y : uint uint -> uint +(define (calc-row-mid-y proc-index row-height) + (floor (- (+ (* proc-index + row-height) + (/ row-height 2)) + 2))) + +;Gets the center of a circle with (xleft, ytop) as the top-left coordinate. +;;calc-center : uint uint uint -> (values uint uint) +(define (calc-center xleft ytop diameter) + (let ([rad (floor (/ diameter 2))]) + (values (+ xleft rad) + (+ ytop rad)))) + +;;segs-equal-or-after : float (listof segment) -> (listof segment) +(define (segs-equal-or-later real-time segs) + (let loop ([sgs segs]) + (cond + [(null? sgs) '()] + [(>= (event-start-time (segment-event (car sgs))) real-time) sgs] + [else (loop (cdr sgs))]))) + +;;segment-edge : segment -> uint +(define (segment-edge seg) + (define evt (segment-event seg)) + (if (event-has-duration? evt) + (segment-x seg) + (+ (segment-x seg) (segment-width seg)))) + +;;calc-ticks : (listof segment) float trace -> (listof timeline-tick) +(define (calc-ticks segs timeToPixMod tr) + (define trace-start (inexact->exact (trace-start-time tr))) + (define segs-len (length segs)) + (define-values (lt lx tks) + (for/fold ([last-time trace-start] + [last-x 0] + [ticks '()]) ([i (in-range 0 (floor (/ (- (trace-end-time tr) + trace-start) + DEFAULT-TIME-INTERVAL)))]) + (define tick-time (+ last-time DEFAULT-TIME-INTERVAL)) + (define tick-rel-time (* (add1 i) DEFAULT-TIME-INTERVAL)) + (define want-x (+ last-x (* DEFAULT-TIME-INTERVAL timeToPixMod))) + (define next-seg (findf (λ (s) (> (event-start-time (segment-event s)) tick-time)) segs)) + (define most-recent-seg (list-ref segs (sub1 (event-index (segment-event next-seg))))) + (define most-recent-evt (segment-event most-recent-seg)) + (define most-recent-time (inexact->exact (event-start-time most-recent-evt))) + (define next-evt (segment-event next-seg)) + (define next-evt-time (inexact->exact (event-start-time next-evt))) + (define most-recent-edge (segment-edge most-recent-seg)) + (define next-edge (segment-x next-seg)) + (cond + [(= most-recent-time tick-time) + (values tick-time + (segment-x most-recent-seg) + (cons (timeline-tick (segment-x most-recent-seg) tick-time tick-rel-time) ticks))] + [(= (segment-x next-seg) (add1 (+ (segment-x most-recent-seg) (segment-width most-recent-seg)))) + (values tick-time + (+ (segment-x most-recent-seg) (segment-width most-recent-seg)) + (cons (timeline-tick (+ (segment-x most-recent-seg) + (segment-width most-recent-seg)) + tick-time + tick-rel-time) + ticks))] + [else + (define start-x (max most-recent-edge last-x)) + (define start-time (max most-recent-time last-time)) + (define size-mod (/ (- next-edge start-x) (- next-evt-time start-time))) + (define x-offset (ceiling (* (- tick-time start-time) size-mod))) + (define tick-x (round (+ start-x x-offset))) + (values tick-time + tick-x + (cons (timeline-tick tick-x tick-time tick-rel-time) ticks))]))) + tks) + +;;calc-process-timespan-lines : trace (listof segment) -> (listof (uint . uint)) +(define (calc-process-timespan-lines trace segs) + (for/list ([tl (in-list (trace-proc-timelines trace))]) + (let ([segs (filter (λ (s) (= (process-timeline-proc-id tl) + (event-proc-id (segment-event s)))) + segs)]) + (cons (segment-x (car segs)) + (segment-x (last segs)))))) + +;;get-first-future-seg : seg -> seg +(define (get-first-future-seg seg) + (let loop ([cur seg]) + (if (segment-prev-future-seg cur) + (loop (segment-prev-future-seg cur)) + cur))) + +;;get-first-future-seg-in-region : viewable-region segment -> segment +(define (get-first-future-seg-in-region vregion seg) + (let loop ([cur seg]) + (let ([prev (segment-prev-future-seg cur)]) + (if (not prev) + cur + (if (not ((seg-in-vregion vregion) prev)) + cur + (loop prev)))))) + +;;adjust-work-segs! : (listof segment) -> void +(define (adjust-work-segs! segs) + (for ([seg (in-list segs)]) + (case (event-type (segment-event seg)) + [(start-work start-0-work) + (set-segment-width! seg (max MIN-SEG-WIDTH + (- (segment-x (segment-next-proc-seg seg)) (segment-x seg))))] + [else + void]))) + +;;connect-segments! : (listof segment) -> void +(define (connect-segments! segs) + (for ([s (in-list segs)]) + (let ([evt (segment-event s)]) + (set-segment-prev-proc-seg! s (if (event-prev-proc-event evt) + (event-segment (event-prev-proc-event evt)) + #f)) + (set-segment-next-proc-seg! s (if (event-next-proc-event evt) + (event-segment (event-next-proc-event evt)) + #f)) + (set-segment-prev-future-seg! s (if (event-prev-future-event evt) + (event-segment (event-prev-future-event evt)) + #f)) + (set-segment-next-future-seg! s (if (event-next-future-event evt) + (event-segment (event-next-future-event evt)) + #f)) + (set-segment-prev-targ-future-seg! s (if (event-prev-targ-future-event evt) + (event-segment (event-prev-targ-future-event evt)) + #f)) + (set-segment-next-targ-future-seg! s (if (event-next-targ-future-event evt) + (event-segment (event-next-targ-future-event evt)) + #f))))) + +(struct acc (delta last-right-edge) #:transparent) + +;;build-seg-layout : flonum (listof event) trace -> (values (listof segment) uint uint) +(define (build-seg-layout timeToPixModifier events tr) + (define last-right-edges (build-vector (length (trace-proc-timelines tr)) (λ (n) 0))) + (define-values (sgs d x-extent) + (for/fold ([segs '()] + [delta 0] + [largest-x 0]) ([evt (in-list events)]) + (define last-right-edge (vector-ref last-right-edges (event-proc-index evt))) + (define wanted-offset (+ delta (* DEFAULT-TIMELINE-WIDTH + (inexact->exact + (/ (- (event-start-time evt) (trace-start-time tr)) + (- (trace-end-time tr) (trace-start-time tr))))))) + (define-values (offset new-delta) + (if (last-right-edge . <= . wanted-offset) + (values wanted-offset delta) + (values last-right-edge (+ delta (- last-right-edge wanted-offset))))) + (define radius (/ MIN-SEG-WIDTH 2)) + (define segw MIN-SEG-WIDTH) + #;(define segw (case (event-type evt) + [(start-work start-0-work) + (max MIN-SEG-WIDTH + (inexact->exact + (round (* timeToPixModifier + (- (event-end-time evt) (event-start-time evt))))))] + [else MIN-SEG-WIDTH])) + (define seg (segment evt + (round offset) + (- (calc-row-mid-y (event-proc-index evt) TIMELINE-ROW-HEIGHT) radius) + segw + MIN-SEG-WIDTH + (get-event-color (event-type evt)) + #f + #f + #f + #f + #f + #f + #f)) + (set-event-segment! evt seg) + (vector-set! last-right-edges (event-proc-index evt) (+ offset segw)) + (values (cons seg segs) + new-delta + (max largest-x last-right-edge)))) + (values sgs x-extent)) + +;;calc-segments : trace uint uint -> (values frame-info (listof segment)) +(define (calc-segments tr) + (define evts (trace-all-events tr)) + (define timeToPixModifier (/ DEFAULT-TIMELINE-WIDTH (- (trace-end-time tr) (trace-start-time tr)))) + (define-values (segments x) + (build-seg-layout timeToPixModifier evts tr)) + (define ordered-segs (reverse segments)) + (connect-segments! ordered-segs) + (adjust-work-segs! ordered-segs) + (define ticks (calc-ticks ordered-segs timeToPixModifier tr)) + (values (frame-info (+ MIN-SEG-WIDTH (round x)) + (* TIMELINE-ROW-HEIGHT (length (trace-proc-timelines tr))) + TIMELINE-ROW-HEIGHT + timeToPixModifier + ticks + (calc-process-timespan-lines tr ordered-segs)) + ordered-segs)) + +;;pict-for-segment : segment -> pict +(define (pict-for-segment seg) + (when (not (segment-p seg)) + (set-segment-p! seg (if (event-has-duration? (segment-event seg)) + (rect-pict (segment-color seg) + (timeline-event-strokecolor) + (segment-width seg) + MIN-SEG-WIDTH + #:stroke-width .5) + (circle-pict (segment-color seg) + (timeline-event-strokecolor) + MIN-SEG-WIDTH + #:stroke-width .5)))) + (segment-p seg)) + +;;draw-ruler-on : pict viewable-region frameinfo -> pict +(define (draw-ruler-on base vregion frameinfo) + (let loop ([pct base] + [ticks (filter (λ (t) (in-viewable-region-horiz vregion (timeline-tick-x t))) + (frame-info-timeline-ticks frameinfo))] + [next-label-x (viewable-region-x-extent vregion)] + [next-tick-x (viewable-region-x-extent vregion)]) + (cond + [(null? ticks) pct] + [(< (- next-tick-x (timeline-tick-x (car ticks))) TIMELINE-MIN-TICK-PADDING) + (loop pct + (cdr ticks) + next-label-x + next-tick-x)] + [else (let* ([LABEL-PAD 2] + [VERT-PAD 3] + [cur-tick (car ticks)] + [cur-x (timeline-tick-x cur-tick)] + [tick-desc (format "~a ms" (real->decimal-string + (timeline-tick-rel-time cur-tick) 1))] + [t (text-block-pict tick-desc + #:backcolor (timeline-tick-label-backcolor) + #:forecolor (timeline-tick-label-forecolor) + #:padding 3)] + [text-width (pict-width t)] + [show-label? (<= (+ cur-x LABEL-PAD text-width) next-label-x)] + [pinnedline (pin-over pct + (- cur-x (viewable-region-x vregion)) + 0 + (linestyle 'dot + (colorize (vline 1 + (frame-info-adjusted-height frameinfo)) + (if show-label? + (timeline-tick-bold-color) + (timeline-tick-color)))))]) + (if show-label? + (loop (pin-over pinnedline + (- cur-x (viewable-region-x vregion)) + VERT-PAD + t) + (cdr ticks) + cur-x + cur-x) + (loop pinnedline + (cdr ticks) + next-label-x + cur-x)))]))) + +;;draw-row-lines-on : pict viewable-region trace frameinfo -> pict +(define (draw-row-lines-on base vregion tr finfo opacity) + (pin-over base + 0 + 0 + (for/fold ([pct base]) ([tl (in-list (trace-proc-timelines tr))] + [i (in-naturals)]) + (let* ([line-coords (list-ref (frame-info-process-line-coords finfo) + (process-timeline-proc-index tl))] + [line-start (car line-coords)] + [line-end (cdr line-coords)] + [vregion-start (viewable-region-x vregion)] + [vregion-end (viewable-region-x-extent vregion)] + [start-x (cond + [(< line-start vregion-start) 0] + [(between line-start vregion-start vregion-end) + (- line-start vregion-start)] + [else vregion-end])] + [end-x (cond + [(< line-end vregion-start) 0] + [(between line-end vregion-start vregion-end) + (- line-end vregion-start)] + [else vregion-end])] + [proc-name (if (zero? i) + "Thread 0 (Runtime Thread)" + (format "Thread ~a" (process-timeline-proc-id tl)))] + [proc-title (text-block-pict proc-name + #:backcolor (header-backcolor) + #:forecolor (header-forecolor) + #:padding HEADER-PADDING + #:opacity opacity + #:width (viewable-region-width vregion))]) + (draw-stack-onto pct + (at 0 + (- (* (add1 i) (frame-info-row-height finfo)) (viewable-region-y vregion)) + (colorize (hline (viewable-region-width vregion) 1) (timeline-baseline-color))) + (at 0 + (+ (+ (- (* i (frame-info-row-height finfo)) (viewable-region-y vregion)) + (- (frame-info-row-height finfo) (pict-height proc-title))) + 1) + proc-title) + (at start-x + (- (calc-row-mid-y (process-timeline-proc-index tl) (frame-info-row-height finfo)) + (viewable-region-y vregion)) + (colorize (hline (- end-x start-x) 1) + (timeline-event-baseline-color)))))))) + +;Magnifies a segment's pict (dot or square) to make +;it stand out when hovered over with the mouse pointer. +;;make-stand-out-pict : segment -> pict +(define (make-stand-out-pict seg) + (case (event-type (segment-event seg)) + [(start-work start-0-work) (scale (segment-p seg) 1 2)] + [else (scale (segment-p seg) 2)])) + +;;frame-bg : viewable-region frame-info trace -> pict +(define (frame-bg vregion finfo tr) + (draw-frame-bg-onto (colorize (filled-rectangle (viewable-region-width vregion) + (frame-info-adjusted-height finfo)) + (timeline-frame-bg-color)) + vregion + finfo + tr + TIMELINE-HEADER-OPACITY)) + +;;draw-frame-bg-onto : pict viewable-region frameinfo trace -> pict +(define (draw-frame-bg-onto base vregion finfo tr opacity) + (let ([with-ruler (draw-ruler-on base vregion finfo)]) + (draw-row-lines-on with-ruler vregion tr finfo opacity))) + +;;draw-connection : viewable-region segment segment pict string [uint bool symbol] -> pict +(define (draw-connection vregion + start + end + base-pct + color + #:width [width 1] + #:with-arrow [with-arrow #f] + #:style [style 'solid]) + (let*-values ([(midx midy) (calc-center (- (segment-x start) (viewable-region-x vregion)) + (- (segment-y start) (viewable-region-y vregion)) + MIN-SEG-WIDTH)] + [(nextx nexty) (calc-center (- (segment-x end) (viewable-region-x vregion)) + (- (segment-y end) (viewable-region-y vregion)) + MIN-SEG-WIDTH)] + [(dx dy) (values (- nextx midx) (- nexty midy))]) + (if (and (zero? dy) + (or (not (eq? (segment-next-proc-seg start) end)) + (< dx CONNECTION-LINE-HAT-THRESHOLD))) + (let* ([dxa (/ dx 2)] + [dya (- HAT-HEIGHT CONNECTION-LINE-HAT-THRESHOLD)] + [breakx (+ midx dxa)] + [breaky (+ midy dya)]) + (draw-line-onto (draw-line-onto base-pct + midx + midy + breakx + breaky + color + #:width width + #:style style) + breakx + breaky + nextx + nexty + color + #:width width + #:with-arrow with-arrow + #:style style)) + (draw-line-onto base-pct + midx + midy + nextx + nexty + color + #:width width + #:with-arrow with-arrow + #:style style)))) + +;;draw-arrows : pict viewable-region segment -> pict +(define (draw-arrows base-pct vregion seg) + (let ([fst (get-first-future-seg-in-region vregion seg)]) + (let loop ([pct base-pct] + [cur-seg fst]) + (if (not cur-seg) + pct + (let ([next (segment-next-future-seg cur-seg)]) + (let* ([next-targ (segment-next-targ-future-seg cur-seg)] + [prev-targ (segment-prev-targ-future-seg cur-seg)] + [ftl-arrows (if (not next) + pct + (draw-connection vregion + cur-seg + next + pct + (event-connection-line-color) + #:width 2))] + [prev-targ-arr (if (not prev-targ) + ftl-arrows + (draw-connection vregion + prev-targ + cur-seg + ftl-arrows + (event-target-future-line-color) + #:with-arrow #t + #:style 'dot))] + [next-targ-arr (if (not next-targ) + prev-targ-arr + (draw-connection vregion + cur-seg + next-targ + prev-targ-arr + (event-target-future-line-color) + #:with-arrow #t + #:style 'dot))]) + (loop next-targ-arr + next))))))) + +;;timeline-bmp-from-log : (listof indexed-fevent) (or uint bool) (or uint bool) -> bitmap% +(define (build-timeline-bmp-from-log logs + #:max-width [max-width #f] + #:max-height [max-height #f]) + (define vregion (if (or (not max-width) (not max-height)) + #f + (viewable-region 0 + 0 + max-width + max-height))) + + (define p (build-timeline-pict-from-log logs vregion)) + (pict->bitmap p)) + +(define (truncate-bmp bmp width height) + (define w (min width (send bmp get-width))) + (define h (min height (send bmp get-height))) + (let ([buf (make-bytes (* width height 4))]) + (send bmp + get-argb-pixels + 0 + 0 + w + h + buf) + (let ([new-b (make-bitmap w h)]) + (send new-b + set-argb-pixels + 0 + 0 + w + h + buf) + new-b))) + +;;build-timeline-bmp-with-overlay : (listof indexed-fevent) uint [uint] [uint] -> bitmap% +(define (build-timeline-bmp-with-overlay logs + event-index + #:max-width [max-width #f] + #:max-height [max-height #f]) + (define p (build-timeline-with-overlay logs event-index)) + (define-values (w h) + (values (if max-width (min max-width (pict-width p)) (pict-width p)) + (if max-height (min max-height (pict-height p)) (pict-height p)))) + (truncate-bmp (pict->bitmap p) w h)) + + +;;build-timeline-pict-from-trace : trace viewable-region -> pict +(define (build-timeline-pict-from-trace tr vregion) + (define-values (finfo segments) (calc-segments tr)) + (build-timeline-pict vregion + tr + finfo + segments)) + +;;build-timeline-pict : (or viewable-region #f) trace frame-info (listof segment) -> pict +(define (build-timeline-pict vregion tr finfo segments) + (define vr (if (not vregion) + (viewable-region 0 + 0 + (frame-info-adjusted-width finfo) + (frame-info-adjusted-height finfo)) + vregion)) + (for/fold ([pct (frame-bg vr finfo tr)]) + ([seg (in-list (filter (seg-in-vregion vr) segments))]) + (pin-over pct + (- (segment-x seg) (viewable-region-x vr)) + (- (segment-y seg) (viewable-region-y vr)) + (pict-for-segment seg)))) + +;;build-timeline-pict-from-log : (listof indexed-fevent) viewable-region -> pict +(define (build-timeline-pict-from-log logs vregion) + (build-timeline-pict-from-trace (build-trace logs) vregion)) + +;;build-timeline-with-overlay : (listof indexed-fevent) uint -> pict +(define (build-timeline-with-overlay logs event-index) + (define tr (build-trace logs)) + (define-values (finfo segments) (calc-segments tr)) + (define vregion (viewable-region-from-frame finfo)) + (define timeline-p (build-timeline-pict vregion + tr + finfo + segments)) + (define overlay (build-timeline-overlay vregion + #f + (list-ref segments event-index) + finfo + tr)) + (pin-over timeline-p + 0 + 0 + overlay)) + +;Draws the pict that is layered on top of the exec. timeline canvas +;to highlight a specific future's event sequence +;;build-timeline-overlay : uint uint (or segment #f) (or segment #f) frame-info trace -> pict +(define (build-timeline-overlay vregion tacked hovered finfo tr) + (define-values (width height) (values (viewable-region-width vregion) + (viewable-region-height vregion))) + (define base (blank (viewable-region-width vregion) + (viewable-region-height vregion))) + (define-values (seg-with-arrows showing-tacked) + (if tacked (values tacked #t) (values hovered #f))) + (if (and seg-with-arrows (segment-p seg-with-arrows)) + (let* ([bg base] + [dots (let loop ([p bg] [cur-seg (get-first-future-seg-in-region vregion seg-with-arrows)]) + (if (or (not cur-seg) (not ((seg-in-vregion vregion) cur-seg))) + p + (loop (pin-over p + (- (segment-x cur-seg) (viewable-region-x vregion)) + (- (segment-y cur-seg) (viewable-region-y vregion)) + (pict-for-segment cur-seg)) + (segment-next-future-seg cur-seg))))] + [aseg-rel-x (- (segment-x seg-with-arrows) (viewable-region-x vregion))] + [aseg-rel-y (- (segment-y seg-with-arrows) (viewable-region-y vregion))] + [line (pin-over dots + (- (+ aseg-rel-x + (/ (segment-width seg-with-arrows) 2)) + 2) + 0 + (colorize (vline 1 height) (hover-tickline-color)))] + [bigger (make-stand-out-pict seg-with-arrows)] + [width-dif (/ (- (pict-width bigger) (pict-width (segment-p seg-with-arrows))) 2)] + [height-dif (/ (- (pict-height bigger) (pict-height (segment-p seg-with-arrows))) 2)] + [magnified (pin-over line + (- aseg-rel-x width-dif) + (- aseg-rel-y height-dif) + bigger)] + [hover-magnified (if (and showing-tacked + hovered + (segment-p hovered) + (not (eq? hovered tacked))) + (let* ([hmag (make-stand-out-pict hovered)] + [hwidth-dif (/ (- (pict-width hmag) + (pict-width (segment-p hovered))) + 2)] + [hheight-dif (/ (- (pict-height hmag) + (pict-height (segment-p hovered))) + 2)]) + (pin-over magnified + (- (- (segment-x hovered) (viewable-region-x vregion)) hwidth-dif) + (- (- (segment-y hovered) (viewable-region-y vregion)) hheight-dif) + hmag)) + magnified)] + [arrows (draw-arrows hover-magnified vregion seg-with-arrows)]) + arrows) + base)) + +;Draw a line from one node on the creation graph to another +;;line-from : drawable-node drawable-node pict viewable-region -> pict +(define (line-from start end pct vregion) + (let* ([par-center (drawable-node-center start)] + [child-center (drawable-node-center end)] + [minx (viewable-region-x vregion)] + [miny (viewable-region-y vregion)]) + (draw-line-onto pct + (- (point-x par-center) minx) + (- (point-y par-center) miny) + (- (point-x child-center) minx) + (- (point-y child-center) miny) + (create-graph-edge-color) + #:width 1 + #:style 'dot))) + +;Draws a circle for a node on the creation graph +;;node-pict : drawable-node -> pict +(define (node-pict dnode) + (let* ([ndata (node-data (drawable-node-node dnode))] + [ntext (if (equal? ndata 'runtime-thread) + "RTT" + (format "~a" (event-user-data ndata)))]) + (cc-superimpose (circle-pict (create-graph-node-backcolor) + (create-graph-node-strokecolor) + (drawable-node-width dnode)) + (colorize (text ntext) (create-graph-node-forecolor))))) + +;Cache the creation graph pict after first drawing +(define cg-pict #f) + +;;draw-creategraph-pict : viewable-region tree-layout -> pict +(define (draw-creategraph-pict vregion layout) + (define rt-root (first (graph-layout-nodes layout))) + (define width (inexact->exact (floor (graph-layout-width layout)))) + (define height (inexact->exact (floor (graph-layout-height layout)))) + (define base (blank (viewable-region-width vregion) (viewable-region-height vregion))) + (define viewable-nodes (filter (λ (n) (in-viewable-region vregion + (drawable-node-x n) + (drawable-node-y n) + (drawable-node-width n) + (drawable-node-width n))) + (graph-layout-nodes layout))) + (let ([arrow-pct (for/fold ([pct base]) ([node (in-list (graph-layout-nodes layout))]) + (for/fold ([p pct]) ([child (in-list (drawable-node-children node))]) + (line-from node child p vregion)))]) + (for/fold ([pct arrow-pct]) ([node (in-list viewable-nodes)]) + (pin-over pct + (- (drawable-node-x node) (viewable-region-x vregion)) + (- (drawable-node-y node) (viewable-region-y vregion)) + (node-pict node))))) + +;Draws the pict displayed in the creation graph canvas +;;creategraph-p : viewable-region tree-layout uint -> pict +(define (build-creategraph-pict vregion layout zoom-level) + (define factor (zoom-level->factor zoom-level)) + (set! cg-pict (draw-creategraph-pict (scale-viewable-region vregion (/ 1 factor)) layout)) + (scale cg-pict factor)) + +(define (zoom-level->factor zoom-level) + (+ 1.0 (* (- zoom-level CREATE-GRAPH-DEFAULT-ZOOM) + CREATE-GRAPH-ZOOM-FACTOR))) + +;;graph-overlay-pict : drawable-node trace graph-layout -> pict +(define (graph-overlay-pict hover-node tr layout vregion) + (when hover-node + (unless (equal? (node-data (drawable-node-node hover-node)) 'runtime-thread) + (define fid (event-user-data (node-data (drawable-node-node hover-node)))) + (define ri (hash-ref (trace-future-rtcalls tr) fid (λ () #f))) + (when ri + (define block-ops (sort (hash-keys (rtcall-info-block-hash ri)) + > + #:key (λ (p) + (hash-ref (rtcall-info-block-hash ri) p)))) + (define sync-ops (sort (hash-keys (rtcall-info-sync-hash ri)) + > + #:key (λ (op) + (hash-ref (rtcall-info-sync-hash ri) op)))) + (define-values (node-origin-x node-origin-y) + (values (- (drawable-node-x hover-node) (viewable-region-x vregion)) + (- (drawable-node-y hover-node) (viewable-region-y vregion)))) + (define-values (center-x center-y) + (values (+ node-origin-x (/ (drawable-node-width hover-node) 2)) + (+ node-origin-y (/ (drawable-node-width hover-node) 2)))) + (define x (+ center-x CREATE-GRAPH-NODE-DIAMETER)) + (define-values (pct yacc) + (for/fold ([p (pin-over (blank (viewable-region-width vregion) (viewable-region-height vregion)) + node-origin-x + node-origin-y + (node-pict hover-node))] + [yacc node-origin-y]) + ([rtcall (in-list (append (map (λ (op) (cons 'block op)) block-ops) + (map (λ (op) (cons 'sync op)) sync-ops)))]) + (define evt-type (car rtcall)) + (define prim (cdr rtcall)) + (define the-hash (if (equal? evt-type 'block) (rtcall-info-block-hash ri) (rtcall-info-sync-hash ri))) + (define txtp (text-pict (format "~a (~a)" + (symbol->string prim) + (hash-ref the-hash prim)) + #:color (get-event-forecolor evt-type))) + (define txtbg (rect-pict (get-event-color evt-type) + (create-graph-edge-color) + (+ (pict-width txtp) (* TOOLTIP-MARGIN 2)) + (+ (pict-height txtp) (* TOOLTIP-MARGIN 2)) + #:stroke-width .5)) + (values + (pin-over (draw-line-onto p + center-x + center-y + x + yacc + (create-graph-edge-color)) + x + yacc + (pin-over txtbg + TOOLTIP-MARGIN + TOOLTIP-MARGIN + txtp)) + (+ yacc (pict-height txtbg) CREATE-GRAPH-PADDING)))) + pct)))) + \ No newline at end of file diff --git a/collects/racket/future/private/visualizer-gui.rkt b/collects/racket/future/private/visualizer-gui.rkt new file mode 100644 index 0000000000..8204dd1ea0 --- /dev/null +++ b/collects/racket/future/private/visualizer-gui.rkt @@ -0,0 +1,361 @@ +#lang racket/gui +(require framework + data/interval-map + mrlib/hierlist + "visualizer-drawing.rkt" + "visualizer-data.rkt" + "gui-helpers.rkt" + "graph-drawing.rkt" + "display.rkt" + "constants.rkt") + +(provide (contract-out [show-visualizer (-> void?)]) + show-visualizer-for-trace) + +;;rebuild-mouse-index : frame-info trace (listof segment) -> interval-map of (range --> interval-map) +(define (rebuild-mouse-index frameinfo tr segs) + (let ([ym (make-interval-map)]) + (for ([tl (in-list (trace-proc-timelines tr))]) + (let* ([xm (make-interval-map)] + [midy (calc-row-mid-y (process-timeline-proc-index tl) (frame-info-row-height frameinfo))] + [miny (floor (- midy (/ MIN-SEG-WIDTH 2)))] + [maxy (floor (+ midy (/ MIN-SEG-WIDTH 2)))]) + (interval-map-set! ym + miny + maxy + xm) + (for ([seg (in-list (filter (λ (s) + (= (event-proc-id (segment-event s)) + (process-timeline-proc-id tl))) + segs))]) + (interval-map-set! xm + (segment-x seg) + (+ (segment-x seg) (segment-width seg)) + seg)))) + ym)) + +;;display-evt-details : segment trace message message message message message message -> void +(define (display-evt-details seg + tr + selected-label + time-label + fid-label + pid-label + data-label1 + data-label2) + (if seg + (let ([evt (segment-event seg)]) + (send selected-label set-label (format "Event: ~a" (event-type evt))) + (send time-label set-label (format "Time: +~a" (get-time-string (- (event-start-time evt) + (trace-start-time tr))))) + (send fid-label set-label (format "Future ID: ~a" (if (not (event-future-id evt)) + "None (top-level event)" + (event-future-id evt)))) + (send pid-label set-label (format "Process ID: ~a" (event-proc-id evt))) + (case (event-type evt) + [(start-work start-0-work) + (send data-label1 set-label (format "Duration: ~a" (get-time-string (- (event-end-time evt) + (event-start-time evt)))))] + [(block sync) + (when (= (event-proc-id evt) RT-THREAD-ID) + (send data-label1 set-label (format "Primitive: ~a" (symbol->string (event-prim-name evt))))) + (when (equal? (event-prim-name evt) 'touch) + (send data-label2 set-label (format "Touching future ~a" (event-user-data evt)))) + (when (equal? (event-prim-name evt) (string->symbol "[allocate memory]")) + (send data-label2 set-label (format "Size: ~a" (event-user-data evt))))] + [(create) + (send data-label1 set-label (format "Creating future ~a" (event-user-data evt)))] + [(touch) + (send data-label1 set-label (format "Touching future ~a" (event-user-data evt)))] + [else + (send data-label1 set-label "")])) + (begin + (send selected-label set-label "") + (send time-label set-label "") + (send fid-label set-label "") + (send pid-label set-label "") + (send data-label1 set-label "") + (send data-label2 set-label "")))) + +(define (get-window-size) + (define-values (screen-w screen-h) (get-display-size)) + (values (min screen-w DEF-WINDOW-WIDTH) + (min screen-h DEF-WINDOW-HEIGHT))) + +(define (show-visualizer-for-trace logs) + ;TODO: Just set initial sizes, not minimum sizes + ;If for some reason the log is empty, error? + (when (empty? logs) + (error 'show-visualizer "No future log messages found.")) + (define the-trace (build-trace logs)) + (define-values (winw winh) (get-window-size)) + ;The event segment we are currently mousing over + (define hover-seg #f) + ;The event segment we last clicked on (tacked) -- if any + (define tacked-seg #f) + ;Table for broadcasting selection events to other controls + (define listener-table (make-listener-table)) + + (define f (new frame:standard-menus% + [label "Futures Performance"] + [width winw] + [height winh])) + (define main-panel (new panel:horizontal-dragable% + [parent (send f get-area-container)])) + (define left-panel (new panel:horizontal-dragable% [parent main-panel] + [stretchable-width #t])) + (define hlist-ctl (new hierarchical-list% + [parent left-panel] + [stretchable-width #t] + [stretchable-height #t] + [style '(control-border)])) + + ;Build up items in the hierlist + (define block-node (send hlist-ctl new-list)) + (send (send block-node get-editor) insert "Blocks" 0) + (for ([prim (in-list (sort (hash-keys (trace-block-counts the-trace)) > #:key (λ (x) (hash-ref (trace-block-counts the-trace) x))))]) + (define item (send block-node new-item)) + (send (send item get-editor) insert (format "~a (~a)" prim (hash-ref (trace-block-counts the-trace) prim)))) + + (define sync-node (send hlist-ctl new-list)) + (send (send sync-node get-editor) insert "Syncs" 0) + (for ([prim (in-list (sort (hash-keys (trace-sync-counts the-trace)) > #:key (λ (x) (hash-ref (trace-sync-counts the-trace) x))))]) + (define item (send sync-node new-item)) + (send (send item get-editor) insert (format "~a (~a)" prim (hash-ref (trace-sync-counts the-trace) prim)))) + + (define right-panel (new panel:vertical-dragable% + [parent main-panel] + [stretchable-width #t])) + (define graphic-panel (new panel:horizontal-dragable% + [parent right-panel] + [stretchable-height #t] + [min-width (inexact->exact (round (* winw .8)))])) + (define timeline-container (new vertical-panel% + [parent graphic-panel] + [stretchable-width #t] + [stretchable-height #t])) + (define graph-container (new vertical-panel% + [parent graphic-panel] + [stretchable-width #t] + [stretchable-height #t])) + (define timeline-header (section-header timeline-container "Execution Timeline" 'horizontal)) + (define graph-header (section-header graph-container "Future Creation Tree" 'horizontal)) + + ;Calculate required sizes, mouse hover index, and create timeline pict container + (define-values (frameinfo segments) (calc-segments the-trace)) + (define timeline-mouse-index (rebuild-mouse-index frameinfo the-trace segments)) + (define timeline-panel (new pict-canvas% + [parent timeline-container] + [redraw-on-resize #f] + [pict-builder (λ (vregion) (build-timeline-pict vregion the-trace frameinfo segments))] + [hover-handler (λ (x y vregion) + (let ([seg (find-seg-for-coords x y timeline-mouse-index)]) + (set! hover-seg seg) + ;(send timeline-panel set-redraw-overlay! #t) + (post-event listener-table 'segment-hover timeline-panel seg)))] + [click-handler (λ (x y vregion) + (let ([seg (find-seg-for-coords x y timeline-mouse-index)]) + (set! tacked-seg seg) + ;(send timeline-panel set-redraw-overlay! #t) + (post-event listener-table 'segment-click timeline-panel seg)))] + [overlay-builder (λ (vregion) + (build-timeline-overlay vregion + tacked-seg + hover-seg + frameinfo + the-trace))] + [min-width 500] + [min-height (inexact->exact (round (* winh .7)))] + [style '(hscroll vscroll)] + [stretchable-width #t])) + (send timeline-panel init-auto-scrollbars + (frame-info-adjusted-width frameinfo) + (frame-info-adjusted-height frameinfo) + 0.0 + 0.0) + (send timeline-panel show-scrollbars #t #t) + (send timeline-panel set-redo-bitmap-on-paint! #t) + + ;Calculate for and create creation graph pict container + (define creation-tree-layout (draw-tree (trace-creation-tree the-trace) + #:node-width CREATE-GRAPH-NODE-DIAMETER + #:padding CREATE-GRAPH-PADDING + #:zoom CREATE-GRAPH-DEFAULT-ZOOM)) + + (define hovered-graph-node #f) + (define creategraph-panel (new pict-canvas% + [parent graph-container] + [redraw-on-resize #f] + [pict-builder (λ (vregion) + (build-creategraph-pict vregion + creation-tree-layout + cg-zoom-level))] + [hover-handler (λ (x y vregion) + (set! hovered-graph-node + (find-node-for-coords x + y + (graph-layout-nodes creation-tree-layout))))] + [click-handler (λ (x y vregion) + (define fid (find-fid-for-coords + x y (graph-layout-nodes creation-tree-layout) + vregion)) + (when fid + (define seg (first-seg-for-fid fid segments)) + (set! tacked-seg seg) + (send timeline-panel set-redraw-overlay! #t) + (send timeline-panel refresh) + (post-event listener-table 'segment-click timeline-panel seg)))] + [overlay-builder (λ (vregion) + (graph-overlay-pict hovered-graph-node + the-trace + creation-tree-layout + vregion))] + [min-width 500] + [min-height 500] + [style '(hscroll vscroll)] + [stretchable-width #t])) + + (send creategraph-panel show-scrollbars #t #t) + (send creategraph-panel init-auto-scrollbars + (inexact->exact (floor (graph-layout-width creation-tree-layout))) + (inexact->exact (floor (graph-layout-height creation-tree-layout))) + 0.0 + 0.0) + (send creategraph-panel set-redo-bitmap-on-paint! #t) + + + (define graph-footer (new horizontal-panel% + [parent graph-container] + [stretchable-width #t] + [stretchable-height #f] + [style '(border)])) + (define cg-zoom-level CREATE-GRAPH-DEFAULT-ZOOM) + + ;;Handles a change event for the creation graph zoom slider + ;;on-zoom : slider% event% -> void + (define (on-zoom slider event) + (printf "slider: ~s\n" (send slider get-value)) ;;REMOVE + (set! cg-zoom-level (send slider get-value)) + (send creategraph-panel redraw-everything)) + + (define zoom-slider (new slider% + [parent graph-footer] + [label "Zoom:"] + [min-value CREATE-GRAPH-MIN-ZOOM] + [max-value CREATE-GRAPH-MAX-ZOOM] + [init-value CREATE-GRAPH-DEFAULT-ZOOM] + [style '(horizontal plain)] + [callback on-zoom])) + (define bottom-panel (new horizontal-panel% + [parent right-panel] + [style '(border)] + [stretchable-height #t])) + + (define left-container (new horizontal-panel% + [parent bottom-panel] + [style '(border)] + [stretchable-height #t])) + (define mid-container (new horizontal-panel% + [parent bottom-panel] + [stretchable-height #t])) + (define right-container (new horizontal-panel% + [parent bottom-panel] + [stretchable-height #t])) + (define left-bot-header (section-header left-container "Execution Statistics" 'vertical)) + (define left-bot-panel (new vertical-panel% + [parent left-container] + [stretchable-height #t])) + (define mid-bot-header (section-header mid-container "Event Details" 'vertical)) + (define mid-bot-panel (new vertical-panel% + [parent mid-container] + [stretchable-height #t])) + (define right-bot-header (section-header right-container "Log Viewer" 'vertical)) + (define right-bot-panel (new vertical-panel% + [parent right-container] + [stretchable-height #t])) + + (bold-label left-bot-panel "Program Statistics") + (define runtime-label (label left-bot-panel + (format "Real time: ~a" (get-time-string (trace-real-time the-trace))))) + (define fcount-label (label left-bot-panel + (format "Total futures: ~a" (trace-num-futures the-trace)))) + (define blocks-label (label left-bot-panel + (format "Barricades: ~a" (trace-num-blocks the-trace)))) + (define syncs-label (label left-bot-panel + (format "Syncs: ~a" (trace-num-syncs the-trace)))) + + ;Selected-event-specific labels + (define hover-label (mt-bold-label mid-bot-panel)) + (define hover-time-label (mt-label mid-bot-panel)) + (define hover-fid-label (mt-label mid-bot-panel)) + (define hover-pid-label (mt-label mid-bot-panel)) + (define hover-data-label1 (mt-label mid-bot-panel)) + (define hover-data-label2 (mt-label mid-bot-panel)) + + (define tacked-label (mt-bold-label mid-bot-panel)) + (define tacked-time-lbl (mt-label mid-bot-panel)) + (define tacked-fid-lbl (mt-label mid-bot-panel)) + (define tacked-pid-lbl (mt-label mid-bot-panel)) + (define tacked-data-lbl (mt-label mid-bot-panel)) + (define tacked-data-lbl2 (mt-label mid-bot-panel)) + + (define (update-event-details-panel seg) + (display-evt-details hover-seg + the-trace + hover-label + hover-time-label + hover-fid-label + hover-pid-label + hover-data-label1 + hover-data-label2) + (display-evt-details tacked-seg + the-trace + tacked-label + tacked-time-lbl + tacked-fid-lbl + tacked-pid-lbl + tacked-data-lbl + tacked-data-lbl2)) + + ;Wire up events so selection, etc. in one panel is communicated to others + (define (on-future-selected fid) + 0) + + (define (on-segment-hover seg) + 0) + + (define (on-segment-click seg) + 0) + + (define (on-segment-unclick seg) + 0) + + ;Wire up event handlers for selection, etc. + (add-receiver listener-table 'future-selected timeline-panel on-future-selected) + (add-receiver listener-table 'segment-hover creategraph-panel on-segment-hover) + (add-receiver listener-table 'segment-click creategraph-panel on-segment-click) + (add-receiver listener-table 'segment-click mid-bot-panel update-event-details-panel) + (add-receiver listener-table 'segment-hover mid-bot-panel update-event-details-panel) + (add-receiver listener-table 'segment-unclick mid-bot-panel update-event-details-panel) + (add-receiver listener-table 'segment-unclick creategraph-panel on-segment-unclick) + + ;Additional menus/items + (define showing-create-graph #t) + (define view-menu (new menu% [label "View"] [parent (send f get-menu-bar)])) + (new menu-item% + [label "Hide Creation Tree"] + [parent view-menu] + [callback (λ (item evt) + (if showing-create-graph + (begin + (send graphic-panel delete-child graph-container) + (send item set-label "Show Creation Tree")) + (begin + (send graphic-panel add-child graph-container) + (send item set-label "Hide Creation Tree"))) + (set! showing-create-graph (not showing-create-graph)))]) + + (send f show #t)) + +(define (show-visualizer) + (show-visualizer-for-trace (raw-log-output 0))) diff --git a/collects/racket/future/visualizer.rkt b/collects/racket/future/visualizer.rkt new file mode 100644 index 0000000000..cbb8c5f7c4 --- /dev/null +++ b/collects/racket/future/visualizer.rkt @@ -0,0 +1,5 @@ +#lang racket/base +(require "private/visualizer-gui.rkt" + "private/visualizer-data.rkt") +(provide start-performance-tracking! + show-visualizer) \ No newline at end of file diff --git a/collects/scribblings/guide/futures.scrbl b/collects/scribblings/guide/futures.scrbl index d3cffaa30d..5ba685e522 100644 --- a/collects/scribblings/guide/futures.scrbl +++ b/collects/scribblings/guide/futures.scrbl @@ -1,7 +1,12 @@ #lang scribble/doc -@(require scribble/manual "guide-utils.rkt" +@(require scribble/manual scribble/eval "guide-utils.rkt" (for-label racket/flonum racket/future)) +@(define future-eval (make-base-eval)) +@(interaction-eval #:eval future-eval (require racket/future + racket/future/private/visualizer-drawing + racket/future/private/visualizer-data)) + @title[#:tag "effective-futures"]{Parallelism with Futures} The @racketmodname[racket/future] library provides support for @@ -56,9 +61,12 @@ l2)] becomes available about the same time that it is demanded by @racket[(touch f)]. Futures run in parallel as long as they can do so safely, but the -notion of ``safe'' for parallelism is inherently tied to the system -implementation. The distinction between ``safe'' and ``unsafe'' +notion of ``future safe'' is inherently tied to the +implementation. The distinction between ``future safe'' and ``future unsafe'' operations may be far from apparent at the level of a Racket program. +The remainder of this section works through an example to illustrate +this distinction and to show how to use the future visualizer +can help shed light on it. Consider the following core of a Mandelbrot-set computation: @@ -72,10 +80,10 @@ Consider the following core of a Mandelbrot-set computation: (let ([zrq (* zr zr)] [ziq (* zi zi)]) (cond - [(> (+ zrq ziq) 4.0) i] + [(> (+ zrq ziq) 4) i] [else (loop (add1 i) (+ (- zrq ziq) cr) - (+ (* 2.0 zr zi) ci))])))))) + (+ (* 2 zr zi) ci))])))))) ] The expressions @racket[(mandelbrot 10000000 62 500 1000)] and @@ -97,35 +105,332 @@ Unfortunately, attempting to run the two computations in parallel with (touch f))) ] -One problem is that the @racket[*] and @racket[/] operations in the -first two lines of @racket[mandelbrot] involve a mixture of exact and -inexact real numbers. Such mixtures typically trigger a slow path in -execution, and the general slow path is not safe for -parallelism. Consequently, the future created in this example is -almost immediately suspended, and it cannot resume until -@racket[touch] is called. +To see why, use the @racketmodname[racket/future/visualizer], like this: -Changing the first two lines of @racket[mandelbrot] addresses that -first the problem: +@racketblock[ + (require racket/future/visualizer) + (start-performance-tracking!) + + (let ([f (future (lambda () (mandelbrot 10000000 62 501 1000)))]) + (list (mandelbrot 10000000 62 500 1000) + (touch f))) + + (show-visualizer)] + +This opens a window showing a graphical view of a trace of the computation. +The upper-left portion of the window contains an execution timeline: + +@(interaction-eval + #:eval future-eval + (define bad-log + (list (indexed-fevent 0 '#s(future-event #f 0 create 1334778390997.936 #f 1)) + (indexed-fevent 1 '#s(future-event 1 1 start-work 1334778390998.137 #f #f)) + (indexed-fevent 2 '#s(future-event 1 1 sync 1334778390998.145 #f #f)) + (indexed-fevent 3 '#s(future-event 1 0 sync 1334778391001.616 [allocate memory] #f)) + (indexed-fevent 4 '#s(future-event 1 0 result 1334778391001.629 #f #f)) + (indexed-fevent 5 '#s(future-event 1 1 result 1334778391001.643 #f #f)) + (indexed-fevent 6 '#s(future-event 1 1 block 1334778391001.653 #f #f)) + (indexed-fevent 7 '#s(future-event 1 1 suspend 1334778391001.658 #f #f)) + (indexed-fevent 8 '#s(future-event 1 1 end-work 1334778391001.658 #f #f)) + (indexed-fevent 9 '#s(future-event 1 0 block 1334778392134.226 > #f)) + (indexed-fevent 10 '#s(future-event 1 0 result 1334778392134.241 #f #f)) + (indexed-fevent 11 '#s(future-event 1 1 start-work 1334778392134.254 #f #f)) + (indexed-fevent 12 '#s(future-event 1 1 sync 1334778392134.339 #f #f)) + (indexed-fevent 13 '#s(future-event 1 0 sync 1334778392134.375 [allocate memory] #f)) + (indexed-fevent 14 '#s(future-event 1 0 result 1334778392134.38 #f #f)) + (indexed-fevent 15 '#s(future-event 1 1 result 1334778392134.387 #f #f)) + (indexed-fevent 16 '#s(future-event 1 1 block 1334778392134.39 #f #f)) + (indexed-fevent 17 '#s(future-event 1 1 suspend 1334778392134.391 #f #f)) + (indexed-fevent 18 '#s(future-event 1 1 end-work 1334778392134.391 #f #f)) + (indexed-fevent 19 '#s(future-event 1 0 touch-pause 1334778392134.432 #f #f)) + (indexed-fevent 20 '#s(future-event 1 0 touch-resume 1334778392134.433 #f #f)) + (indexed-fevent 21 '#s(future-event 1 0 block 1334778392134.533 * #f)) + (indexed-fevent 22 '#s(future-event 1 0 result 1334778392134.537 #f #f)) + (indexed-fevent 23 '#s(future-event 1 2 start-work 1334778392134.568 #f #f)) + (indexed-fevent 24 '#s(future-event 1 2 sync 1334778392134.57 #f #f)) + (indexed-fevent 25 '#s(future-event 1 0 touch-pause 1334778392134.587 #f #f)) + (indexed-fevent 26 '#s(future-event 1 0 touch-resume 1334778392134.587 #f #f)) + (indexed-fevent 27 '#s(future-event 1 0 block 1334778392134.6 [allocate memory] #f)) + (indexed-fevent 28 '#s(future-event 1 0 result 1334778392134.604 #f #f)) + (indexed-fevent 29 '#s(future-event 1 2 result 1334778392134.627 #f #f)) + (indexed-fevent 30 '#s(future-event 1 2 block 1334778392134.629 #f #f)) + (indexed-fevent 31 '#s(future-event 1 2 suspend 1334778392134.632 #f #f)) + (indexed-fevent 32 '#s(future-event 1 2 end-work 1334778392134.633 #f #f)) + (indexed-fevent 33 '#s(future-event 1 0 touch-pause 1334778392134.64 #f #f)) + (indexed-fevent 34 '#s(future-event 1 0 touch-resume 1334778392134.64 #f #f)) + (indexed-fevent 35 '#s(future-event 1 0 block 1334778392134.663 > #f)) + (indexed-fevent 36 '#s(future-event 1 0 result 1334778392134.666 #f #f)) + (indexed-fevent 37 '#s(future-event 1 1 start-work 1334778392134.673 #f #f)) + (indexed-fevent 38 '#s(future-event 1 1 block 1334778392134.676 #f #f)) + (indexed-fevent 39 '#s(future-event 1 1 suspend 1334778392134.677 #f #f)) + (indexed-fevent 40 '#s(future-event 1 1 end-work 1334778392134.677 #f #f)) + (indexed-fevent 41 '#s(future-event 1 0 touch-pause 1334778392134.704 #f #f)) + (indexed-fevent 42 '#s(future-event 1 0 touch-resume 1334778392134.704 #f #f)) + (indexed-fevent 43 '#s(future-event 1 0 block 1334778392134.727 * #f)) + (indexed-fevent 44 '#s(future-event 1 0 result 1334778392134.73 #f #f)) + (indexed-fevent 45 '#s(future-event 1 2 start-work 1334778392134.737 #f #f)) + (indexed-fevent 46 '#s(future-event 1 2 block 1334778392134.739 #f #f)) + (indexed-fevent 47 '#s(future-event 1 2 suspend 1334778392134.74 #f #f)) + (indexed-fevent 48 '#s(future-event 1 2 end-work 1334778392134.741 #f #f)) + (indexed-fevent 49 '#s(future-event 1 0 touch-pause 1334778392134.767 #f #f)) + (indexed-fevent 50 '#s(future-event 1 0 touch-resume 1334778392134.767 #f #f)) + (indexed-fevent 51 '#s(future-event 1 0 block 1334778392134.79 > #f)) + (indexed-fevent 52 '#s(future-event 1 0 result 1334778392134.793 #f #f)) + (indexed-fevent 53 '#s(future-event 1 1 start-work 1334778392134.799 #f #f)) + (indexed-fevent 54 '#s(future-event 1 1 block 1334778392134.801 #f #f)) + (indexed-fevent 55 '#s(future-event 1 1 suspend 1334778392134.802 #f #f)) + (indexed-fevent 56 '#s(future-event 1 1 end-work 1334778392134.803 #f #f)) + (indexed-fevent 57 '#s(future-event 1 0 touch-pause 1334778392134.832 #f #f)) + (indexed-fevent 58 '#s(future-event 1 0 touch-resume 1334778392134.832 #f #f)) + (indexed-fevent 59 '#s(future-event 1 0 block 1334778392134.854 * #f)) + (indexed-fevent 60 '#s(future-event 1 0 result 1334778392134.858 #f #f)) + (indexed-fevent 61 '#s(future-event 1 2 start-work 1334778392134.864 #f #f)) + (indexed-fevent 62 '#s(future-event 1 2 block 1334778392134.876 #f #f)) + (indexed-fevent 63 '#s(future-event 1 2 suspend 1334778392134.877 #f #f)) + (indexed-fevent 64 '#s(future-event 1 2 end-work 1334778392134.882 #f #f)) + (indexed-fevent 65 '#s(future-event 1 0 touch-pause 1334778392134.918 #f #f)) + (indexed-fevent 66 '#s(future-event 1 0 touch-resume 1334778392134.918 #f #f)) + (indexed-fevent 67 '#s(future-event 1 0 block 1334778392134.94 > #f)) + (indexed-fevent 68 '#s(future-event 1 0 result 1334778392134.943 #f #f)) + (indexed-fevent 69 '#s(future-event 1 1 start-work 1334778392134.949 #f #f)) + (indexed-fevent 70 '#s(future-event 1 1 block 1334778392134.952 #f #f)) + (indexed-fevent 71 '#s(future-event 1 1 suspend 1334778392134.953 #f #f)) + (indexed-fevent 72 '#s(future-event 1 1 end-work 1334778392134.96 #f #f)) + (indexed-fevent 73 '#s(future-event 1 0 touch-pause 1334778392134.991 #f #f)) + (indexed-fevent 74 '#s(future-event 1 0 touch-resume 1334778392134.991 #f #f)) + (indexed-fevent 75 '#s(future-event 1 0 block 1334778392135.013 * #f)) + (indexed-fevent 76 '#s(future-event 1 0 result 1334778392135.016 #f #f)) + (indexed-fevent 77 '#s(future-event 1 2 start-work 1334778392135.027 #f #f)) + (indexed-fevent 78 '#s(future-event 1 2 block 1334778392135.033 #f #f)) + (indexed-fevent 79 '#s(future-event 1 2 suspend 1334778392135.034 #f #f)) + (indexed-fevent 80 '#s(future-event 1 2 end-work 1334778392135.04 #f #f)) + (indexed-fevent 81 '#s(future-event 1 0 touch-pause 1334778392135.075 #f #f)) + (indexed-fevent 82 '#s(future-event 1 0 touch-resume 1334778392135.075 #f #f)) + (indexed-fevent 83 '#s(future-event 1 0 block 1334778392135.098 > #f)) + (indexed-fevent 84 '#s(future-event 1 0 result 1334778392135.101 #f #f)) + (indexed-fevent 85 '#s(future-event 1 1 start-work 1334778392135.107 #f #f)) + (indexed-fevent 86 '#s(future-event 1 1 block 1334778392135.117 #f #f)) + (indexed-fevent 87 '#s(future-event 1 1 suspend 1334778392135.118 #f #f)) + (indexed-fevent 88 '#s(future-event 1 1 end-work 1334778392135.123 #f #f)) + (indexed-fevent 89 '#s(future-event 1 0 touch-pause 1334778392135.159 #f #f)) + (indexed-fevent 90 '#s(future-event 1 0 touch-resume 1334778392135.159 #f #f)) + (indexed-fevent 91 '#s(future-event 1 0 block 1334778392135.181 * #f)) + (indexed-fevent 92 '#s(future-event 1 0 result 1334778392135.184 #f #f)) + (indexed-fevent 93 '#s(future-event 1 2 start-work 1334778392135.19 #f #f)) + (indexed-fevent 94 '#s(future-event 1 2 block 1334778392135.191 #f #f)) + (indexed-fevent 95 '#s(future-event 1 2 suspend 1334778392135.192 #f #f)) + (indexed-fevent 96 '#s(future-event 1 2 end-work 1334778392135.192 #f #f)) + (indexed-fevent 97 '#s(future-event 1 0 touch-pause 1334778392135.221 #f #f)) + (indexed-fevent 98 '#s(future-event 1 0 touch-resume 1334778392135.221 #f #f)) + (indexed-fevent 99 '#s(future-event 1 0 block 1334778392135.243 > #f)) + ))) + +@interaction-eval-show[ + #:eval future-eval + (build-timeline-bmp-from-log bad-log + #:max-width 600 + #:max-height 300) +] + +Each horizontal row represents an OS-level thread, and the colored +dots represent important events in the execution of the program (they are +color-coded to distinguish one event type from another). The upper-left blue +dot in the timeline represents the future's creation. The future +executes for a brief period (represented by a green bar in the second line) on thread +1, and then pauses to allow the runtime thread to perform a future-unsafe operation. + +In the Racket implementation, future-unsafe operations fall into one of two categories. +A @deftech{blocking} operation halts the evaluation of the future, and will not allow +it to continue until it is touched. After the operation completes within @racket[touch], +the remainder of the future's work will be evaluated sequentially by the runtime +thread. A @deftech{synchronized} operation also halts the future, but the runtime thread +may perform the operation at any time and, once completed, the future may continue +running in parallel. Memory allocation and JIT compilation are two common examples +of synchronized operations. + +In the timeline, we see an orange dot just to the right of the green bar on thread 1 -- +this dot represents a synchronized operation (memory allocation). The first orange +dot on thread 0 shows that the runtime thread performed the allocation shortly after +the future paused. A short time later, the future halts on a blocking operation +(the first red dot) and must wait until the @racket[touch] for it to be evaluated +(slightly after the 1049ms mark). + +When you move your mouse over an event, the visualizer shows you +detailed information about the event and draws arrows +connecting all of the events in the corresponding future. +This image shows those connections for our future. + +@interaction-eval-show[ + #:eval future-eval + (build-timeline-bmp-with-overlay bad-log + 6 + #:max-width 600 + #:max-height 300) +] + +The dotted orange line connects the first event in the future to +the future that created it, and the purple lines connect adjacent +events within the future. + +The reason that we see no parallelism is that the @racket[<] and @racket[*] operations +in the lower portion of the loop in @racket[mandelbrot] involve a mixture of +floating-point and fixed (integer) values. Such mixtures typically trigger a slow +path in execution, and the general slow path will usually be blocking. + +Changing constants to be floating-points numbers in @racket[mandelbrot] addresses that +first problem: @racketblock[ (define (mandelbrot iterations x y n) - (let ([ci (- (/ (* 2.0 (->fl y)) (->fl n)) 1.0)] - [cr (- (/ (* 2.0 (->fl x)) (->fl n)) 1.5)]) - ....)) + (let ([ci (- (/ (* 2.0 y) n) 1.0)] + [cr (- (/ (* 2.0 x) n) 1.5)]) + (let loop ([i 0] [zr 0.0] [zi 0.0]) + (if (> i iterations) + i + (let ([zrq (* zr zr)] + [ziq (* zi zi)]) + (cond + [(> (+ zrq ziq) 4.0) i] + [else (loop (add1 i) + (+ (- zrq ziq) cr) + (+ (* 2.0 zr zi) ci))])))))) ] -With that change, @racket[mandelbrot] computations can run in -parallel. Nevertheless, performance still does not improve. The -problem is that most every arithmetic operation in this example -produces an inexact number whose storage must be allocated. Especially -frequent allocation triggers communication between parallel tasks that -defeats any performance improvement. +With that change, @racket[mandelbrot] computations can run in +parallel. Nevertheless, we still see a special type of +slow-path operation limiting our parallelism (orange dots): + +@interaction-eval[ + #:eval future-eval + (define better-log + (list (indexed-fevent 0 '#s(future-event #f 0 create 1334779296782.22 #f 2)) + (indexed-fevent 1 '#s(future-event 2 2 start-work 1334779296782.265 #f #f)) + (indexed-fevent 2 '#s(future-event 2 2 sync 1334779296782.378 #f #f)) + (indexed-fevent 3 '#s(future-event 2 0 sync 1334779296795.582 [allocate memory] #f)) + (indexed-fevent 4 '#s(future-event 2 0 result 1334779296795.587 #f #f)) + (indexed-fevent 5 '#s(future-event 2 2 result 1334779296795.6 #f #f)) + (indexed-fevent 6 '#s(future-event 2 2 sync 1334779296795.689 #f #f)) + (indexed-fevent 7 '#s(future-event 2 0 sync 1334779296795.807 [allocate memory] #f)) + (indexed-fevent 8 '#s(future-event 2 0 result 1334779296795.812 #f #f)) + (indexed-fevent 9 '#s(future-event 2 2 result 1334779296795.818 #f #f)) + (indexed-fevent 10 '#s(future-event 2 2 sync 1334779296795.827 #f #f)) + (indexed-fevent 11 '#s(future-event 2 0 sync 1334779296806.627 [allocate memory] #f)) + (indexed-fevent 12 '#s(future-event 2 0 result 1334779296806.635 #f #f)) + (indexed-fevent 13 '#s(future-event 2 2 result 1334779296806.646 #f #f)) + (indexed-fevent 14 '#s(future-event 2 2 sync 1334779296806.879 #f #f)) + (indexed-fevent 15 '#s(future-event 2 0 sync 1334779296806.994 [allocate memory] #f)) + (indexed-fevent 16 '#s(future-event 2 0 result 1334779296806.999 #f #f)) + (indexed-fevent 17 '#s(future-event 2 2 result 1334779296807.007 #f #f)) + (indexed-fevent 18 '#s(future-event 2 2 sync 1334779296807.023 #f #f)) + (indexed-fevent 19 '#s(future-event 2 0 sync 1334779296814.198 [allocate memory] #f)) + (indexed-fevent 20 '#s(future-event 2 0 result 1334779296814.206 #f #f)) + (indexed-fevent 21 '#s(future-event 2 2 result 1334779296814.221 #f #f)) + (indexed-fevent 22 '#s(future-event 2 2 sync 1334779296814.29 #f #f)) + (indexed-fevent 23 '#s(future-event 2 0 sync 1334779296820.796 [allocate memory] #f)) + (indexed-fevent 24 '#s(future-event 2 0 result 1334779296820.81 #f #f)) + (indexed-fevent 25 '#s(future-event 2 2 result 1334779296820.835 #f #f)) + (indexed-fevent 26 '#s(future-event 2 2 sync 1334779296821.089 #f #f)) + (indexed-fevent 27 '#s(future-event 2 0 sync 1334779296825.217 [allocate memory] #f)) + (indexed-fevent 28 '#s(future-event 2 0 result 1334779296825.226 #f #f)) + (indexed-fevent 29 '#s(future-event 2 2 result 1334779296825.242 #f #f)) + (indexed-fevent 30 '#s(future-event 2 2 sync 1334779296825.305 #f #f)) + (indexed-fevent 31 '#s(future-event 2 0 sync 1334779296832.541 [allocate memory] #f)) + (indexed-fevent 32 '#s(future-event 2 0 result 1334779296832.549 #f #f)) + (indexed-fevent 33 '#s(future-event 2 2 result 1334779296832.562 #f #f)) + (indexed-fevent 34 '#s(future-event 2 2 sync 1334779296832.667 #f #f)) + (indexed-fevent 35 '#s(future-event 2 0 sync 1334779296836.269 [allocate memory] #f)) + (indexed-fevent 36 '#s(future-event 2 0 result 1334779296836.278 #f #f)) + (indexed-fevent 37 '#s(future-event 2 2 result 1334779296836.326 #f #f)) + (indexed-fevent 38 '#s(future-event 2 2 sync 1334779296836.396 #f #f)) + (indexed-fevent 39 '#s(future-event 2 0 sync 1334779296843.481 [allocate memory] #f)) + (indexed-fevent 40 '#s(future-event 2 0 result 1334779296843.49 #f #f)) + (indexed-fevent 41 '#s(future-event 2 2 result 1334779296843.501 #f #f)) + (indexed-fevent 42 '#s(future-event 2 2 sync 1334779296843.807 #f #f)) + (indexed-fevent 43 '#s(future-event 2 0 sync 1334779296847.291 [allocate memory] #f)) + (indexed-fevent 44 '#s(future-event 2 0 result 1334779296847.3 #f #f)) + (indexed-fevent 45 '#s(future-event 2 2 result 1334779296847.312 #f #f)) + (indexed-fevent 46 '#s(future-event 2 2 sync 1334779296847.375 #f #f)) + (indexed-fevent 47 '#s(future-event 2 0 sync 1334779296854.487 [allocate memory] #f)) + (indexed-fevent 48 '#s(future-event 2 0 result 1334779296854.495 #f #f)) + (indexed-fevent 49 '#s(future-event 2 2 result 1334779296854.507 #f #f)) + (indexed-fevent 50 '#s(future-event 2 2 sync 1334779296854.656 #f #f)) + (indexed-fevent 51 '#s(future-event 2 0 sync 1334779296857.374 [allocate memory] #f)) + (indexed-fevent 52 '#s(future-event 2 0 result 1334779296857.383 #f #f)) + (indexed-fevent 53 '#s(future-event 2 2 result 1334779296857.421 #f #f)) + (indexed-fevent 54 '#s(future-event 2 2 sync 1334779296857.488 #f #f)) + (indexed-fevent 55 '#s(future-event 2 0 sync 1334779296869.919 [allocate memory] #f)) + (indexed-fevent 56 '#s(future-event 2 0 result 1334779296869.947 #f #f)) + (indexed-fevent 57 '#s(future-event 2 2 result 1334779296869.981 #f #f)) + (indexed-fevent 58 '#s(future-event 2 2 sync 1334779296870.32 #f #f)) + (indexed-fevent 59 '#s(future-event 2 0 sync 1334779296879.438 [allocate memory] #f)) + (indexed-fevent 60 '#s(future-event 2 0 result 1334779296879.446 #f #f)) + (indexed-fevent 61 '#s(future-event 2 2 result 1334779296879.463 #f #f)) + (indexed-fevent 62 '#s(future-event 2 2 sync 1334779296879.526 #f #f)) + (indexed-fevent 63 '#s(future-event 2 0 sync 1334779296882.928 [allocate memory] #f)) + (indexed-fevent 64 '#s(future-event 2 0 result 1334779296882.935 #f #f)) + (indexed-fevent 65 '#s(future-event 2 2 result 1334779296882.944 #f #f)) + (indexed-fevent 66 '#s(future-event 2 2 sync 1334779296883.311 #f #f)) + (indexed-fevent 67 '#s(future-event 2 0 sync 1334779296890.471 [allocate memory] #f)) + (indexed-fevent 68 '#s(future-event 2 0 result 1334779296890.479 #f #f)) + (indexed-fevent 69 '#s(future-event 2 2 result 1334779296890.517 #f #f)) + (indexed-fevent 70 '#s(future-event 2 2 sync 1334779296890.581 #f #f)) + (indexed-fevent 71 '#s(future-event 2 0 sync 1334779296894.362 [allocate memory] #f)) + (indexed-fevent 72 '#s(future-event 2 0 result 1334779296894.369 #f #f)) + (indexed-fevent 73 '#s(future-event 2 2 result 1334779296894.382 #f #f)) + (indexed-fevent 74 '#s(future-event 2 2 sync 1334779296894.769 #f #f)) + (indexed-fevent 75 '#s(future-event 2 0 sync 1334779296901.501 [allocate memory] #f)) + (indexed-fevent 76 '#s(future-event 2 0 result 1334779296901.51 #f #f)) + (indexed-fevent 77 '#s(future-event 2 2 result 1334779296901.556 #f #f)) + (indexed-fevent 78 '#s(future-event 2 2 sync 1334779296901.62 #f #f)) + (indexed-fevent 79 '#s(future-event 2 0 sync 1334779296905.428 [allocate memory] #f)) + (indexed-fevent 80 '#s(future-event 2 0 result 1334779296905.434 #f #f)) + (indexed-fevent 81 '#s(future-event 2 2 result 1334779296905.447 #f #f)) + (indexed-fevent 82 '#s(future-event 2 2 sync 1334779296905.743 #f #f)) + (indexed-fevent 83 '#s(future-event 2 0 sync 1334779296912.538 [allocate memory] #f)) + (indexed-fevent 84 '#s(future-event 2 0 result 1334779296912.547 #f #f)) + (indexed-fevent 85 '#s(future-event 2 2 result 1334779296912.564 #f #f)) + (indexed-fevent 86 '#s(future-event 2 2 sync 1334779296912.625 #f #f)) + (indexed-fevent 87 '#s(future-event 2 0 sync 1334779296916.094 [allocate memory] #f)) + (indexed-fevent 88 '#s(future-event 2 0 result 1334779296916.1 #f #f)) + (indexed-fevent 89 '#s(future-event 2 2 result 1334779296916.108 #f #f)) + (indexed-fevent 90 '#s(future-event 2 2 sync 1334779296916.243 #f #f)) + (indexed-fevent 91 '#s(future-event 2 0 sync 1334779296927.233 [allocate memory] #f)) + (indexed-fevent 92 '#s(future-event 2 0 result 1334779296927.242 #f #f)) + (indexed-fevent 93 '#s(future-event 2 2 result 1334779296927.262 #f #f)) + (indexed-fevent 94 '#s(future-event 2 2 sync 1334779296927.59 #f #f)) + (indexed-fevent 95 '#s(future-event 2 0 sync 1334779296934.603 [allocate memory] #f)) + (indexed-fevent 96 '#s(future-event 2 0 result 1334779296934.612 #f #f)) + (indexed-fevent 97 '#s(future-event 2 2 result 1334779296934.655 #f #f)) + (indexed-fevent 98 '#s(future-event 2 2 sync 1334779296934.72 #f #f)) + (indexed-fevent 99 '#s(future-event 2 0 sync 1334779296938.773 [allocate memory] #f)) + )) +] + +@interaction-eval-show[ + #:eval future-eval + (build-timeline-bmp-from-log better-log #:max-width 600 #:max-height 300) +] + +The problem is that most every arithmetic operation in this example +produces an inexact number whose storage must be allocated. While some allocation +can safely be performed exclusively without the aid of the runtime thread, especially +frequent allocation requires synchronized operations which defeat any performance +improvement. By using @tech{flonum}-specific operations (see -@secref["fixnums+flonums"]), we can re-write @racket[mandelbot] to use +@secref["fixnums+flonums"]), we can re-write @racket[mandelbrot] to use much less allocation: +@interaction-eval[ + #:eval future-eval + (define good-log + (list (indexed-fevent 0 '#s(future-event #f 0 create 1334778395768.733 #f 3)) + (indexed-fevent 1 '#s(future-event 3 2 start-work 1334778395768.771 #f #f)) + (indexed-fevent 2 '#s(future-event 3 2 complete 1334778395864.648 #f #f)) + (indexed-fevent 3 '#s(future-event 3 2 end-work 1334778395864.652 #f #f)) + )) +] + @racketblock[ (define (mandelbrot iterations x y n) (let ([ci (fl- (fl/ (* 2.0 (->fl y)) (->fl n)) 1.0)] @@ -145,42 +450,23 @@ much less allocation: This conversion can speed @racket[mandelbrot] by a factor of 8, even in sequential mode, but avoiding allocation also allows @racket[mandelbrot] to run usefully faster in parallel. +Executing this program yields the following in the visualizer: + +@interaction-eval-show[ + #:eval future-eval + (build-timeline-bmp-from-log good-log + #:max-width 600 + #:max-height 300) +] + +Notice that only one green bar is shown here because one of the +mandelbrot computations is not being evaluated by a future (on +the runtime thread). As a general guideline, any operation that is inlined by the @tech{JIT} compiler runs safely in parallel, while other operations that are not inlined (including all operations if the JIT compiler is -disabled) are considered unsafe. The @exec{mzc} decompiler tool +disabled) are considered unsafe. The @exec{raco decompile} tool annotates operations that can be inlined by the compiler (see @secref[#:doc '(lib "scribblings/raco/raco.scrbl") "decompile"]), so the decompiler can be used to help predict parallel performance. - -To more directly report what is happening in a program that uses -@racket[future] and @racket[touch], operations are logged when they -suspend a computation or synchronize with the main computation. For -example, running the original @racket[mandelbrot] in a future produces -the following output in the @racket['debug] log level: - -@margin-note{To see @racket['debug] logging output on stderr, set the -@envvar{PLTSTDERR} environment variable to @tt{debug} or start -@exec{racket} with @Flag{W} @tt{debug}.} - -@verbatim[#:indent 2]|{ - future 1, process 1: BLOCKING on process 0; time: .... - .... - future 1, process 0: HANDLING: *; time: .... -}| - -The messages indicate which internal future-running task became -blocked on an unsafe operation, the time it blocked (in terms of -@racket[current-inexact-miliseconds]), and the operation that caused -the computation it to block. - -The first revision to @racket[mandelbrot] avoids suspending at -@racket[*], but produces many log entries of the form - -@verbatim[#:indent 2]|{ - future 1, process 0: synchronizing: [allocate memory]; time: .... -}| - -The @tt{[allocate memory]} part of the message indicates that -synchronization was needed for memory allocation. diff --git a/collects/scribblings/guide/mand-bad-hover.png b/collects/scribblings/guide/mand-bad-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..5fa5300ba96f986c27751a5b695ebd2b044eebe3 GIT binary patch literal 27421 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 literal 0 HcmV?d00001 diff --git a/collects/scribblings/guide/mand-good.png b/collects/scribblings/guide/mand-good.png new file mode 100644 index 0000000000000000000000000000000000000000..ed34b47c1a3df1bbdd82b6ff03ea8cb48ed79d5d GIT binary patch literal 25615 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ℜ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 z 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 literal 0 HcmV?d00001 diff --git a/collects/scribblings/reference/concurrency.scrbl b/collects/scribblings/reference/concurrency.scrbl index 15ac304602..bb544e7a20 100644 --- a/collects/scribblings/reference/concurrency.scrbl +++ b/collects/scribblings/reference/concurrency.scrbl @@ -17,5 +17,6 @@ support for parallelism to improve performance. @include-section["sync.scrbl"] @include-section["thread-local.scrbl"] @include-section["futures.scrbl"] +@include-section["futures-visualizer.scrbl"] @include-section["places.scrbl"] @include-section["distributed.scrbl"] diff --git a/collects/scribblings/reference/futures-visualizer.scrbl b/collects/scribblings/reference/futures-visualizer.scrbl new file mode 100644 index 0000000000..0d33d4a29f --- /dev/null +++ b/collects/scribblings/reference/futures-visualizer.scrbl @@ -0,0 +1,86 @@ +#lang scribble/doc +@(require "mz.rkt" #;(for-label racket/future/visualizer)) + +@title[#:tag "futures-visualizer"]{Futures Visualizer} + +@guideintro["effective-futures"]{the future visualizer} + +@defmodule[racket/future/visualizer] + +The @deftech{futures visualizer} is a graphical profiling tool +for parallel programs written using @racket[future]. The tool +shows a timeline of a program's execution including all future-related +events, as well as the overall amount of processor utilization +at any point during the program's lifetime. + +@deftogether[( + @defproc[(start-performance-tracking!) void?] + @defproc[(show-visualizer) void?] +)]{ + The @racket[start-performance-tracking!] procedure enables the collection + of data required by the visualizer. This function should be called immediately + prior to executing code the programmer wishes to profile. + + The @racket[show-visualizer] procedure displays the profiler window. + + A typical program using profiling might look like the following: + + @racketblock[ + (require racket/future + racket/future/visualizer) + + (start-performance-tracking!) + (let ([f (future (lambda () ...))]) + ... + (touch f)) + + (show-visualizer) + ] +} + +@section[#:tag "future-visualizer-timeline"]{Execution Timeline} + +The @deftech{execution timeline}, shown in the top left-hand corner of the +profiler window, displays a history of the program +and all events associated with its futures, with OS-level threads +or @deftech{processes} organized along the y-axis and time increasing along +the x-axis. A coloring convention is used to distinguish between +different types of events (see @secref["future-logging"] for a full +description of these event types): + +@itemlist[ + @item{Blue dot: @racket['create]} + + @item{Green bar: @racket['start-work], @racket['start-0-work]} + + @item{Orange dot: @racket['sync]} + + @item{Red dot: @racket['block], @racket['touch]} + + @item{White dot: @racket['result], @racket['end-work]} + + @item{Green dot: @racket['touch-pause], @racket['touch-resume]} +] + +Mousing over any event connects it via purple lines to the sequence +of events for its future. Additionally, orange dotted lines +with arrowheads may be shown to indicate operations performed from +one future to another (e.g. @racket['create] or @racket['touch] actions). +To view details about two events simultaneously, a selection +can be tacked by clicking the mouse. + +The timeline displays vertical lines at 100-microsecond intervals. Note that +though the time interval is fixed, the pixel distance between lines varies +based on the event density for any given time range to prevent overlapping +event circles. + +@section[#:tag "future-visualizer-tree"]{Future Creation Tree} + +The @deftech{creation tree} shows a tree with a single node per +future created by the program. This display can be particularly useful +for programs which spawn futures in nested fashion (futures within futures). +For any given future node, the children +of that node represent futures which were created by that future (within +the scope of its thunk). For all programs, the root of the tree +is a special node representing the main computation thread (the runtime thread), +and is denoted @deftech{RTT}. diff --git a/collects/scribblings/reference/futures.scrbl b/collects/scribblings/reference/futures.scrbl index 35574cade0..eec3b5a565 100644 --- a/collects/scribblings/reference/futures.scrbl +++ b/collects/scribblings/reference/futures.scrbl @@ -183,16 +183,16 @@ Racket futures use logging (see @secref["logging"]) extensively to report information about how futures are evaluated. Logging output is useful for debugging the performance of programs that use futures. +Though textual log output can be viewed directly, it is much +easier to use the graphical profiler tool provided by +@racketmodname[racket/future/visualizer]. + In addition to its string message, each event logged for a future has a data value that is an instance of a @racket[future-event] @tech{prefab} structure: @racketblock[ -(define-struct future-event (future-id - proc-id - action - time - unsafe-op-name) +(define-struct future-event (future-id proc-id action time unsafe-op-name target-fid) #:prefab) ] @@ -281,7 +281,13 @@ In process 0, some event pairs can be nested within other event pairs: An @racket[block] in process 0 is generated when an unsafe operation is handled. This type of event will contain a symbol in the @racket[unsafe-op-name] field that is the name of the operation. In all -other cases, this field contains @racket[#f].} +other cases, this field contains @racket[#f]. + +The @racket[target-fid] field contains an exact integer value in certain +cases where the @racket[action] occurs in one future but is being +performed on another (e.g. @racket['create] or @racket['touch]). In such +cases, the integer value is the identifier of the future on which the action +is being performed. In all other cases, this field contains @racket[#f]. @; ---------------------------------------------------------------------- diff --git a/collects/tests/future/bad-trace1.rkt b/collects/tests/future/bad-trace1.rkt new file mode 100644 index 0000000000..63ee6eeae2 --- /dev/null +++ b/collects/tests/future/bad-trace1.rkt @@ -0,0 +1,105 @@ +#lang racket +(require racket/future/private/visualizer-data) +(provide BAD-TRACE-1) + +(define BAD-TRACE-1 (list (indexed-fevent 0 '#s(future-event #f 0 create 1334779294212.415 #f 1)) +(indexed-fevent 1 '#s(future-event 1 1 start-work 1334779294212.495 #f #f)) +(indexed-fevent 2 '#s(future-event 1 1 sync 1334779294212.501 #f #f)) +(indexed-fevent 3 (future-event 1 0 'sync 1334779294221.128 'allocate_memory #f)) +(indexed-fevent 4 '#s(future-event 1 0 result 1334779294221.138 #f #f)) +(indexed-fevent 5 '#s(future-event 1 1 result 1334779294221.15 #f #f)) +(indexed-fevent 6 '#s(future-event 1 1 block 1334779294221.158 #f #f)) +(indexed-fevent 7 '#s(future-event 1 1 suspend 1334779294221.164 #f #f)) +(indexed-fevent 8 '#s(future-event 1 1 end-work 1334779294221.165 #f #f)) +(indexed-fevent 9 '#s(future-event 1 0 block 1334779295356.267 > #f)) +(indexed-fevent 10 '#s(future-event 1 0 result 1334779295356.293 #f #f)) +(indexed-fevent 11 '#s(future-event 1 1 start-work 1334779295356.316 #f #f)) +(indexed-fevent 12 '#s(future-event 1 1 sync 1334779295356.448 #f #f)) +(indexed-fevent 13 '#s(future-event 1 0 touch-pause 1334779295356.451 #f #f)) +(indexed-fevent 14 '#s(future-event 1 0 touch-resume 1334779295356.472 #f #f)) +(indexed-fevent 15 (future-event 1 0 'block 1334779295356.936 'allocate_memory #f)) +(indexed-fevent 16 '#s(future-event 1 0 result 1334779295356.959 #f #f)) +(indexed-fevent 17 '#s(future-event 1 1 result 1334779295356.97 #f #f)) +(indexed-fevent 18 '#s(future-event 1 1 block 1334779295356.988 #f #f)) +(indexed-fevent 19 '#s(future-event 1 1 suspend 1334779295356.99 #f #f)) +(indexed-fevent 20 '#s(future-event 1 1 end-work 1334779295357.0 #f #f)) +(indexed-fevent 21 '#s(future-event 1 0 touch-pause 1334779295357.066 #f #f)) +(indexed-fevent 22 '#s(future-event 1 0 touch-resume 1334779295357.066 #f #f)) +(indexed-fevent 23 '#s(future-event 1 0 block 1334779295357.104 * #f)) +(indexed-fevent 24 '#s(future-event 1 0 result 1334779295357.11 #f #f)) +(indexed-fevent 25 '#s(future-event 1 1 start-work 1334779295357.119 #f #f)) +(indexed-fevent 26 '#s(future-event 1 1 block 1334779295357.122 #f #f)) +(indexed-fevent 27 '#s(future-event 1 1 suspend 1334779295357.123 #f #f)) +(indexed-fevent 28 '#s(future-event 1 1 end-work 1334779295357.124 #f #f)) +(indexed-fevent 29 '#s(future-event 1 0 touch-pause 1334779295357.174 #f #f)) +(indexed-fevent 30 '#s(future-event 1 0 touch-resume 1334779295357.174 #f #f)) +(indexed-fevent 31 '#s(future-event 1 0 block 1334779295357.21 > #f)) +(indexed-fevent 32 '#s(future-event 1 0 result 1334779295357.216 #f #f)) +(indexed-fevent 33 '#s(future-event 1 1 start-work 1334779295357.224 #f #f)) +(indexed-fevent 34 '#s(future-event 1 1 block 1334779295357.226 #f #f)) +(indexed-fevent 35 '#s(future-event 1 1 suspend 1334779295357.227 #f #f)) +(indexed-fevent 36 '#s(future-event 1 1 end-work 1334779295357.228 #f #f)) +(indexed-fevent 37 '#s(future-event 1 0 touch-pause 1334779295357.278 #f #f)) +(indexed-fevent 38 '#s(future-event 1 0 touch-resume 1334779295357.279 #f #f)) +(indexed-fevent 39 '#s(future-event 1 0 block 1334779295357.315 * #f)) +(indexed-fevent 40 '#s(future-event 1 0 result 1334779295357.32 #f #f)) +(indexed-fevent 41 '#s(future-event 1 1 start-work 1334779295357.328 #f #f)) +(indexed-fevent 42 '#s(future-event 1 1 block 1334779295357.337 #f #f)) +(indexed-fevent 43 '#s(future-event 1 1 suspend 1334779295357.338 #f #f)) +(indexed-fevent 44 '#s(future-event 1 1 end-work 1334779295357.347 #f #f)) +(indexed-fevent 45 '#s(future-event 1 0 touch-pause 1334779295357.407 #f #f)) +(indexed-fevent 46 '#s(future-event 1 0 touch-resume 1334779295357.408 #f #f)) +(indexed-fevent 47 '#s(future-event 1 0 block 1334779295357.445 > #f)) +(indexed-fevent 48 '#s(future-event 1 0 result 1334779295357.45 #f #f)) +(indexed-fevent 49 '#s(future-event 1 1 start-work 1334779295357.458 #f #f)) +(indexed-fevent 50 '#s(future-event 1 1 block 1334779295357.46 #f #f)) +(indexed-fevent 51 '#s(future-event 1 1 suspend 1334779295357.461 #f #f)) +(indexed-fevent 52 '#s(future-event 1 1 end-work 1334779295357.477 #f #f)) +(indexed-fevent 53 '#s(future-event 1 0 touch-pause 1334779295357.536 #f #f)) +(indexed-fevent 54 '#s(future-event 1 0 touch-resume 1334779295357.537 #f #f)) +(indexed-fevent 55 '#s(future-event 1 0 block 1334779295357.573 * #f)) +(indexed-fevent 56 '#s(future-event 1 0 result 1334779295357.579 #f #f)) +(indexed-fevent 57 '#s(future-event 1 1 start-work 1334779295357.587 #f #f)) +(indexed-fevent 58 '#s(future-event 1 1 block 1334779295357.595 #f #f)) +(indexed-fevent 59 '#s(future-event 1 1 suspend 1334779295357.596 #f #f)) +(indexed-fevent 60 '#s(future-event 1 1 end-work 1334779295357.597 #f #f)) +(indexed-fevent 61 '#s(future-event 1 0 touch-pause 1334779295357.644 #f #f)) +(indexed-fevent 62 '#s(future-event 1 0 touch-resume 1334779295357.645 #f #f)) +(indexed-fevent 63 '#s(future-event 1 0 block 1334779295357.68 > #f)) +(indexed-fevent 64 '#s(future-event 1 0 result 1334779295357.685 #f #f)) +(indexed-fevent 65 '#s(future-event 1 1 start-work 1334779295357.693 #f #f)) +(indexed-fevent 66 '#s(future-event 1 1 block 1334779295357.695 #f #f)) +(indexed-fevent 67 '#s(future-event 1 1 suspend 1334779295357.697 #f #f)) +(indexed-fevent 68 '#s(future-event 1 1 end-work 1334779295357.712 #f #f)) +(indexed-fevent 69 '#s(future-event 1 0 touch-pause 1334779295357.771 #f #f)) +(indexed-fevent 70 '#s(future-event 1 0 touch-resume 1334779295357.771 #f #f)) +(indexed-fevent 71 '#s(future-event 1 0 block 1334779295357.807 * #f)) +(indexed-fevent 72 '#s(future-event 1 0 result 1334779295357.813 #f #f)) +(indexed-fevent 73 '#s(future-event 1 1 start-work 1334779295357.821 #f #f)) +(indexed-fevent 74 '#s(future-event 1 1 block 1334779295357.823 #f #f)) +(indexed-fevent 75 '#s(future-event 1 1 suspend 1334779295357.824 #f #f)) +(indexed-fevent 76 '#s(future-event 1 1 end-work 1334779295357.84 #f #f)) +(indexed-fevent 77 '#s(future-event 1 0 touch-pause 1334779295357.899 #f #f)) +(indexed-fevent 78 '#s(future-event 1 0 touch-resume 1334779295357.899 #f #f)) +(indexed-fevent 79 '#s(future-event 1 0 block 1334779295357.936 > #f)) +(indexed-fevent 80 '#s(future-event 1 0 result 1334779295357.941 #f #f)) +(indexed-fevent 81 '#s(future-event 1 1 start-work 1334779295357.949 #f #f)) +(indexed-fevent 82 '#s(future-event 1 1 block 1334779295357.957 #f #f)) +(indexed-fevent 83 '#s(future-event 1 1 suspend 1334779295357.958 #f #f)) +(indexed-fevent 84 '#s(future-event 1 1 end-work 1334779295357.971 #f #f)) +(indexed-fevent 85 '#s(future-event 1 0 touch-pause 1334779295358.03 #f #f)) +(indexed-fevent 86 '#s(future-event 1 0 touch-resume 1334779295358.03 #f #f)) +(indexed-fevent 87 '#s(future-event 1 0 block 1334779295358.067 * #f)) +(indexed-fevent 88 '#s(future-event 1 0 result 1334779295358.072 #f #f)) +(indexed-fevent 89 '#s(future-event 1 1 start-work 1334779295358.08 #f #f)) +(indexed-fevent 90 '#s(future-event 1 1 block 1334779295358.088 #f #f)) +(indexed-fevent 91 '#s(future-event 1 1 suspend 1334779295358.09 #f #f)) +(indexed-fevent 92 '#s(future-event 1 1 end-work 1334779295358.099 #f #f)) +(indexed-fevent 93 '#s(future-event 1 0 touch-pause 1334779295358.15 #f #f)) +(indexed-fevent 94 '#s(future-event 1 0 touch-resume 1334779295358.15 #f #f)) +(indexed-fevent 95 '#s(future-event 1 0 block 1334779295358.187 > #f)) +(indexed-fevent 96 '#s(future-event 1 0 result 1334779295358.192 #f #f)) +(indexed-fevent 97 '#s(future-event 1 1 start-work 1334779295358.2 #f #f)) +(indexed-fevent 98 '#s(future-event 1 1 block 1334779295358.209 #f #f)) +(indexed-fevent 99 '#s(future-event 1 1 suspend 1334779295358.21 #f #f)) +)) diff --git a/collects/tests/future/future.rkt b/collects/tests/future/future.rkt index d2ed59f75f..4bf15599f6 100644 --- a/collects/tests/future/future.rkt +++ b/collects/tests/future/future.rkt @@ -15,7 +15,7 @@ We should also test deep continuations. |# ;Tests specific to would-be-future -(define-struct future-event (future-id process-id what time prim-name) +(define-struct future-event (future-id process-id what time prim-name target-fid) #:prefab) (define (get-events-of-type type log) diff --git a/collects/tests/future/visualizer.rkt b/collects/tests/future/visualizer.rkt new file mode 100644 index 0000000000..8741a8f24d --- /dev/null +++ b/collects/tests/future/visualizer.rkt @@ -0,0 +1,273 @@ +#lang racket/base +(require rackunit + racket/vector + racket/future/private/visualizer-drawing + racket/future/private/visualizer-data + racket/future/private/display + "bad-trace1.rkt") + +(define (compile-trace-data logs) + (define tr (build-trace logs)) + (define-values (finfo segs) (calc-segments tr)) + (values tr + finfo + segs + (frame-info-timeline-ticks finfo))) + +;Display tests +(let ([vr (viewable-region 3 3 500 500)]) + (for ([i (in-range 4 503)]) + (check-true (in-viewable-region-horiz vr i) + (format "~a should be in ~a" + i + vr))) + (for ([i (in-range 0 2)]) + (check-false (in-viewable-region-horiz vr i) + (format "~a should not be in ~a" + i + vr)) + (for ([i (in-range 504 1000)]) + (check-false (in-viewable-region-horiz vr i) + (format "~a should not be in ~a" + i + vr))))) + +(let ([vr (viewable-region 0 0 732 685)]) + (check-true (in-viewable-region-horiz vr 10)) + (check-true (in-viewable-region-horiz vr 63.0)) + (check-true (in-viewable-region-horiz vr 116.0)) + (check-true (in-viewable-region-horiz vr 169.0)) + (check-true (in-viewable-region-horiz vr 222))) + +(let ([vr (viewable-region 0 0 732 685)] + [ticks (list (timeline-tick 222.0 #f 0.4999999999999982) + (timeline-tick 169.0 #f 0.3999999999999986) + (timeline-tick 116.0 #f 0.29999999999999893) + (timeline-tick 63.0 #f 0.1999999999999993) + (timeline-tick 10 #f 0.09999999999999964))]) + (define in-vr (filter (λ (t) + (in-viewable-region-horiz vr (timeline-tick-x t))) + ticks)) + (check-equal? (length in-vr) 5)) + +;Trace compilation tests +(let* ([future-log (list (indexed-fevent 0 (future-event 0 0 'create 0 #f 0)) + (indexed-fevent 1 (future-event 0 1 'start-work 1 #f #f)) + (indexed-fevent 2 (future-event 0 1 'end-work 2 #f #f)) + (indexed-fevent 3 (future-event 0 0 'complete 3 #f #f)))] + [organized (organize-output future-log)]) + (check-equal? (vector-length organized) 2) + (let ([proc0log (vector-ref organized 0)] + [proc1log (vector-ref organized 1)]) + (check-equal? (vector-length proc0log) 2) + (check-equal? (vector-length proc1log) 2))) + +(let* ([future-log (list (indexed-fevent 0 (future-event #f 0 'create 0 #f 0)) + (indexed-fevent 1 (future-event 0 1 'start-work 1 #f #f)) + (indexed-fevent 2 (future-event 0 1 'end-work 2 #f #f)) + (indexed-fevent 3 (future-event 0 0 'complete 3 #f #f)))] + [trace (build-trace future-log)] + [evts (trace-all-events trace)]) + (check-equal? (length evts) 4) + (check-equal? (length (filter (λ (e) (event-next-future-event e)) evts)) 2) + (check-equal? (length (filter (λ (e) (event-next-targ-future-event e)) evts)) 1) + (check-equal? (length (filter (λ (e) (event-prev-targ-future-event e)) evts)) 1)) + +(let* ([future-log (list (indexed-fevent 0 (future-event 0 0 'create 0 #f 0)) + (indexed-fevent 1 (future-event 1 0 'create 1 #f 1)) + (indexed-fevent 2 (future-event 0 1 'start-work 2 #f #f)) + (indexed-fevent 3 (future-event 1 2 'start-work 2 #f #f)) + (indexed-fevent 4 (future-event 0 1 'end-work 4 #f #f)) + (indexed-fevent 5 (future-event 0 0 'complete 5 #f #f)) + (indexed-fevent 6 (future-event 1 2 'end-work 5 #f #f)) + (indexed-fevent 7 (future-event 1 0 'complete 7 #f #f)))] + [organized (organize-output future-log)]) + (check-equal? (vector-length organized) 3) + (let ([proc0log (vector-ref organized 0)] + [proc1log (vector-ref organized 1)] + [proc2log (vector-ref organized 2)]) + (check-equal? (vector-length proc0log) 4) + (check-equal? (vector-length proc1log) 2) + (check-equal? (vector-length proc2log) 2) + (for ([msg (in-vector (vector-map indexed-fevent-fevent proc0log))]) + (check-equal? (future-event-process-id msg) 0)) + (for ([msg (in-vector (vector-map indexed-fevent-fevent proc1log))]) + (check-equal? (future-event-process-id msg) 1)) + (for ([msg (in-vector (vector-map indexed-fevent-fevent proc2log))]) + (check-equal? (future-event-process-id msg) 2)))) + +;Drawing calculation tests +(let* ([future-log (list (indexed-fevent 0 (future-event #f 0 'create 0 #f 0)) + (indexed-fevent 1 (future-event 0 1 'start-work 1 #f #f)) + (indexed-fevent 2 (future-event 0 1 'end-work 2 #f #f)) + (indexed-fevent 3 (future-event 0 0 'complete 3 #f #f)))] + [trace (build-trace future-log)]) + (let-values ([(finfo segments) (calc-segments trace)]) + (check-equal? (length segments) 4) + (check-equal? (length (filter (λ (s) (segment-next-future-seg s)) segments)) 2) + (check-equal? (length (filter (λ (s) (segment-next-targ-future-seg s)) segments)) 1) + (check-equal? (length (filter (λ (s) (segment-prev-targ-future-seg s)) segments)) 1))) + +;Future=42 +(let* ([future-log (list (indexed-fevent 0 (future-event #f 0 'create 0.05 #f 42)) + (indexed-fevent 1 (future-event 42 1 'start-work 0.07 #f #f)) + (indexed-fevent 2 (future-event 42 1 'end-work 0.3 #f #f)) + (indexed-fevent 3 (future-event 42 0 'complete 1.2 #f #f)))] + [tr (build-trace future-log)]) + (define-values (finfo segs) (calc-segments tr)) + (define ticks (frame-info-timeline-ticks finfo)) + (check-equal? (length ticks) 11)) + +(define (check-seg-layout tr segs ticks) + (define (do-seg-check seg tick op adjective) + (define evt (segment-event seg)) + (check-true (op (segment-x seg) (timeline-tick-x tick)) + (format "Event at time ~a [x:~a] (~a) should be ~a tick at time ~a [x:~a]" + (relative-time tr (event-start-time evt)) + (segment-x seg) + (event-type evt) + adjective + (exact->inexact (timeline-tick-rel-time tick)) + (timeline-tick-x tick)))) + (for ([seg (in-list segs)]) + (let* ([evt (segment-event seg)] + [evt-rel-time (relative-time tr (event-start-time evt))]) + (for ([tick (in-list ticks)]) + (let ([ttime (timeline-tick-rel-time tick)]) + (cond + [(< evt-rel-time ttime) + (do-seg-check seg tick <= "before")] + [(= evt-rel-time ttime) + (do-seg-check seg tick = "equal to")] + [(> evt-rel-time ttime) + (do-seg-check seg tick >= "after")])))))) + +;Test layout for 'bad' mandelbrot trace +(let-values ([(tr finfo segs ticks) (compile-trace-data BAD-TRACE-1)]) + (check-seg-layout tr segs ticks)) + +(let* ([future-log (list (indexed-fevent 0 (future-event #f 0 'create 0.05 #f 42)) + (indexed-fevent 1 (future-event 42 1 'start-work 0.09 #f #f)) + (indexed-fevent 2 (future-event 42 1 'suspend 1.1 #f #f)) + (indexed-fevent 3 (future-event 42 1 'resume 1.101 #f #f)) + (indexed-fevent 4 (future-event 42 1 'suspend 1.102 #f #f)) + (indexed-fevent 5 (future-event 42 1 'resume 1.103 #f #f)) + (indexed-fevent 6 (future-event 42 1 'start-work 1.104 #f #f)) + (indexed-fevent 7 (future-event 42 1 'complete 1.41 #f #f)) + (indexed-fevent 8 (future-event 42 1 'end-work 1.42 #f #f)) + (indexed-fevent 9 (future-event 42 0 'result 1.43 #f #f)))] + [tr (build-trace future-log)]) + (define-values (finfo segs) (calc-segments tr)) + (define ticks (frame-info-timeline-ticks finfo)) + (check-seg-layout tr segs ticks)) + +(let* ([future-log (list (indexed-fevent 0 (future-event #f 0 'create 0 #f 0)) + (indexed-fevent 1 (future-event 0 1 'start-work 1 #f #f)) + (indexed-fevent 2 (future-event 0 1 'end-work 2 #f #f)) + (indexed-fevent 3 (future-event 0 0 'complete 3 #f #f)))] + [trace (build-trace future-log)]) + (check-equal? (trace-start-time trace) 0) + (check-equal? (trace-end-time trace) 3) + (check-equal? (length (trace-proc-timelines trace)) 2) + (check-equal? (trace-real-time trace) 3) + (check-equal? (trace-num-futures trace) 1) + (check-equal? (trace-num-blocks trace) 0) + (check-equal? (trace-num-syncs trace) 0) + (let ([proc0tl (list-ref (trace-proc-timelines trace) 0)] + [proc1tl (list-ref (trace-proc-timelines trace) 1)]) + (check-equal? (process-timeline-start-time proc0tl) 0) + (check-equal? (process-timeline-end-time proc0tl) 3) + (check-equal? (process-timeline-start-time proc1tl) 1) + (check-equal? (process-timeline-end-time proc1tl) 2) + (let ([proc0segs (process-timeline-events proc0tl)] + [proc1segs (process-timeline-events proc1tl)]) + (check-equal? (length proc0segs) 2) + (check-equal? (length proc1segs) 2) + (check-equal? (event-timeline-position (list-ref proc0segs 0)) 'start) + (check-equal? (event-timeline-position (list-ref proc0segs 1)) 'end)))) + +;Viewable region tests +(define (make-seg-at x y w h) + (segment #f x y w h #f #f #f #f #f #f #f #f)) + +;;make-segs-with-times : (listof (or float (float . float))) -> (listof segment) +(define (make-segs-with-times . times) + (for/list ([t (in-list times)] [i (in-naturals)]) + (if (pair? t) + (make-seg-with-time i (car t) #:end-time (cdr t)) + (make-seg-with-time i t)))) + +;;make-seg-with-time : fixnum float [float] -> segment +(define (make-seg-with-time index real-start-time #:end-time [real-end-time real-start-time]) + (segment (event index + real-start-time + real-end-time + 0 0 0 0 0 0 0 0 0 0 0 0 0 #f) + 0 0 0 0 #f #f #f #f #f #f #f #f)) + + +(let ([vregion (viewable-region 20 30 100 100)] + [seg1 (make-seg-at 0 5 10 10)] + [seg2 (make-seg-at 20 30 5 5)] + [seg3 (make-seg-at 150 35 5 5)]) + (check-false ((seg-in-vregion vregion) seg1)) + (check-true ((seg-in-vregion vregion) seg2)) + (check-false ((seg-in-vregion vregion) seg3))) + +;segs-equal-or-later +(let ([segs (make-segs-with-times 0.1 + 0.3 + 1.2 + (cons 1.4 1.9) + 2.4 + 2.8 + 3.1)]) + (check-equal? (length (segs-equal-or-later 0.1 segs)) 7) + (check-equal? (length (segs-equal-or-later 0.3 segs)) 6) + (check-equal? (length (segs-equal-or-later 1.2 segs)) 5) + (check-equal? (length (segs-equal-or-later 1.4 segs)) 4) + (check-equal? (length (segs-equal-or-later 2.4 segs)) 3) + (check-equal? (length (segs-equal-or-later 2.8 segs)) 2) + (check-equal? (length (segs-equal-or-later 3.1 segs)) 1) + (check-equal? (length (segs-equal-or-later 4.0 segs)) 0)) + +;Tick drawing +(let ([l (list (indexed-fevent 0 (future-event #f 0 'create 10.0 #f 0)) + (indexed-fevent 1 (future-event 0 0 'start-work 11.0 #f #f)) + (indexed-fevent 2 (future-event 0 0 'end-work 20.0 #f #f)))]) + (define-values (tr finfo segs ticks) (compile-trace-data l)) + ;Check that number of ticks stays constant whatever the time->pixel modifier + (check-equal? (length ticks) 100) + (check-equal? (length (calc-ticks segs 700 tr)) 100) + (for ([i (in-range 0.1 20)]) + (check-equal? (length (calc-ticks segs i tr)) + 100 + (format "Wrong number of ticks for time->pix mod ~a\n" i))) + (check-seg-layout tr segs ticks)) + +(let ([l (list (indexed-fevent 0 '#s(future-event #f 0 create 1334778395768.733 #f 3)) + (indexed-fevent 1 '#s(future-event 3 2 start-work 1334778395768.771 #f #f)) + (indexed-fevent 2 '#s(future-event 3 2 complete 1334778395864.648 #f #f)) + (indexed-fevent 3 '#s(future-event 3 2 end-work 1334778395864.652 #f #f)))]) + (define-values (tr finfo segs ticks) (compile-trace-data l)) + (define last-evt (indexed-fevent-fevent (list-ref l 3))) + (define first-evt (indexed-fevent-fevent (list-ref l 0))) + (define total-time (- (future-event-time last-evt) (future-event-time first-evt))) + (check-equal? (length ticks) (inexact->exact (floor (* 10 total-time))))) + +(define mand-first + (list (indexed-fevent 0 '#s(future-event #f 0 create 1334779294212.415 #f 1)) + (indexed-fevent 1 '#s(future-event 1 1 start-work 1334779294212.495 #f #f)) + (indexed-fevent 2 '#s(future-event 1 1 sync 1334779294212.501 #f #f)) + (indexed-fevent 3 (future-event 1 0 'sync 1334779294221.128 'allocate_memory #f)) + (indexed-fevent 4 '#s(future-event 1 0 result 1334779294221.138 #f #f)) + (indexed-fevent 5 '#s(future-event 1 1 result 1334779294221.15 #f #f)))) +(let-values ([(tr finfo segs ticks) (compile-trace-data mand-first)]) + (check-seg-layout tr segs ticks)) + + + + + + + diff --git a/qsort2.rkt b/qsort2.rkt new file mode 100644 index 0000000000..a6bf7e37a9 --- /dev/null +++ b/qsort2.rkt @@ -0,0 +1,98 @@ +#lang racket/base + +(require racket/future + racket/future/visualizer + rackunit + racket/fixnum + racket/unsafe/ops + (only-in racket/list empty?) + racket/vector + rackunit) + +(define (qsort v) + (define length (vector-length v)) + (qsort-h v 0 (sub1 length))) + +(define (qsort-h v l r) + (when (unsafe-fx> (unsafe-fx- r l) 0) + (define m (partition v l r)) + (qsort-h v l (unsafe-fx- m 1)) + (qsort-h v (unsafe-fx+ 1 m) r))) + +(define (swap! v i j) + (define temp (unsafe-vector-ref v j)) + (unsafe-vector-set! v j (unsafe-vector-ref v i)) + (unsafe-vector-set! v i temp)) + +(define (partition v l r) + (swap! v l r) + (define p (unsafe-vector-ref v r)) + ;moving this inline eliminates jit copilations + ;but slows things down and makes things less parallel (factor of ~1.5) + (let recur + ([i l] + [p-i l]) + (cond + [(unsafe-fx= i r) + (swap! v r p-i) + p-i] + [(unsafe-fx< (unsafe-vector-ref v i) p) + (swap! v i p-i) + (recur (unsafe-fx+ 1 i) (unsafe-fx+ p-i 1))] + [else + (recur (unsafe-fx+ 1 i) p-i)]))) + +;(define (partition v l r) +; (let recur ([i (fx- l 1)] +; [j l]) +; (cond +; [(fx= j (fx- r 1)) +; (swap! v (fx+ i 1) r) +; (fx+ i 1)] +; [(fx< (unsafe-vector-ref v j) (unsafe-vector-ref v r)) +; (swap! v (fx+ i 1) j) +; (recur (fx+ i 1) (fx+ j 1))] +; [else +; (recur i (fx+ j 1))]))) + +(define (qsort-f v) + (define length (vector-length v)) + (qsort-f-h v 0 (sub1 length) 6)) + +(define (qsort-f-h v l r d) + (when (unsafe-fx> (unsafe-fx- r l) 0) + ;(define m (partition v l r)) + (if (unsafe-fx= d 0) + (let ([m (partition v l r)]) + (qsort-h v l (unsafe-fx- m 1)) + (qsort-h v (unsafe-fx+ 1 m) r)) + (let* ([m (partition v l r)] + [f1 (future (λ () (qsort-f-h v l (unsafe-fx- m 1) (unsafe-fx- d 1))))] + [f2 (future (λ () (qsort-f-h v (unsafe-fx+ 1 m) r (unsafe-fx- d 1))))]) + (touch f1) + (touch f2))))) + +(define (check-sorting v) + (define l (vector->list v)) + (define v1 (list->vector (sort l <))) + (qsort-f v) + (check-equal? v1 v)) + +(define (rand-v size) (build-vector size (λ (_) (random 30)))) + +;(check-sorting (rand-v 100)) + +(random-seed 2) + +(define v1 (rand-v 50000)) +(define v2 (vector-copy v1)) + +#;(time (begin (sort (vector->list v1) <) + 'a)) +#;(time (begin (qsort v1) + 'b)) +(let () + (start-performance-tracking!) + (time (qsort-f v2)) + (show-visualizer) + 'c) diff --git a/src/racket/src/future.c b/src/racket/src/future.c index 9dc48c1095..49db9dfb01 100644 --- a/src/racket/src/future.c +++ b/src/racket/src/future.c @@ -39,7 +39,7 @@ Scheme_Object *scheme_fsemaphore_p(int argc, Scheme_Object *argv[]) static Scheme_Object *futures_enabled(int argc, Scheme_Object *argv[]) { -#ifdef MZ_USE_FUTURES +#ifdef MZ_USE_FUTURESRACKET return scheme_true; #else return scheme_false; @@ -312,6 +312,7 @@ static int capture_future_continuation(struct Scheme_Future_State *fs, future_t #define FUTURE_RUNSTACK_SIZE 2000 #define FEVENT_BUFFER_SIZE 512 +#define NO_FUTURE_ID -1 enum { FEVENT_CREATE, @@ -589,6 +590,7 @@ void futures_init(void) rt_fts->is_runtime_thread = 1; rt_fts->gen0_size = 1; scheme_future_thread_state = rt_fts; + rt_fts->thread = scheme_current_thread; REGISTER_SO(fs->future_queue); REGISTER_SO(fs->future_queue_end); @@ -617,7 +619,7 @@ void futures_init(void) syms[FEVENT_HANDLE_RTCALL] = sym; sym = scheme_intern_symbol("future-event"); - stype = scheme_lookup_prefab_type(sym, 5); + stype = scheme_lookup_prefab_type(sym, 6); fs->fevent_prefab = stype; init_fevent(&fs->runtime_fevents); @@ -968,8 +970,8 @@ static void free_fevent(Fevent_Buffer *b) } } -static void record_fevent(int what, int fid) XFORM_SKIP_PROC -/* call with the lock or in the runtime thread */ +static void record_fevent_with_data(int what, int fid, int data) + XFORM_SKIP_PROC { Scheme_Future_Thread_State *fts = scheme_future_thread_state; Fevent_Buffer *b; @@ -985,6 +987,7 @@ static void record_fevent(int what, int fid) XFORM_SKIP_PROC b->a[b->pos].timestamp = get_future_timestamp(); b->a[b->pos].what = what; b->a[b->pos].fid = fid; + b->a[b->pos].data = data; b->pos++; if (b->pos == FEVENT_BUFFER_SIZE) { @@ -993,6 +996,12 @@ static void record_fevent(int what, int fid) XFORM_SKIP_PROC } } +static void record_fevent(int what, int fid) XFORM_SKIP_PROC +/* call with the lock or in the runtime thread */ +{ + record_fevent_with_data(what, fid, 0); +} + static void init_traversal(Fevent_Buffer *b) { if (b->overflow) { @@ -1010,18 +1019,19 @@ static void end_traversal(Fevent_Buffer *b) b->pos = 0; } -static void log_future_event(Scheme_Future_State *fs, - const char *msg_str, - const char *extra_str, - int which, - int what, - double timestamp, - int fid) +static void log_future_event_with_data(Scheme_Future_State *fs, + const char *msg_str, + const char *extra_str, + int which, + int what, + double timestamp, + int fid, + int user_data) { Scheme_Object *data, *v; data = scheme_make_blank_prefab_struct_instance(fs->fevent_prefab); - if (what == FEVENT_MISSING) + if (what == FEVENT_MISSING || fid == NO_FUTURE_ID) ((Scheme_Structure *)data)->slots[0] = scheme_false; else ((Scheme_Structure *)data)->slots[0] = scheme_make_integer(fid); @@ -1034,11 +1044,14 @@ static void log_future_event(Scheme_Future_State *fs, ((Scheme_Structure *)data)->slots[2] = v; v = scheme_make_double(timestamp); ((Scheme_Structure *)data)->slots[3] = v; - if (what == FEVENT_HANDLE_RTCALL) { + if (what == FEVENT_HANDLE_RTCALL || what == FEVENT_HANDLE_RTCALL_ATOMIC) { v = scheme_intern_symbol(extra_str); ((Scheme_Structure *)data)->slots[4] = v; } else ((Scheme_Structure *)data)->slots[4] = scheme_false; + + /* User data (target fid for creates, alloc amount for allocation */ + ((Scheme_Structure *)data)->slots[5] = scheme_make_integer(user_data); scheme_log_w_data(scheme_main_logger, SCHEME_LOG_DEBUG, 0, data, @@ -1048,6 +1061,25 @@ static void log_future_event(Scheme_Future_State *fs, fevent_long_strs[what], extra_str, timestamp); + +} + +static void log_future_event(Scheme_Future_State *fs, + const char *msg_str, + const char *extra_str, + int which, + int what, + double timestamp, + int fid) +{ + log_future_event_with_data(fs, + msg_str, + extra_str, + which, + what, + timestamp, + fid, + 0); } static void log_overflow_event(Scheme_Future_State *fs, int which, double timestamp) @@ -1135,13 +1167,14 @@ static void flush_future_logs(Scheme_Future_State *fs) if (!min_b) break; - log_future_event(fs, + log_future_event_with_data(fs, "future %d, process %d: %s%s; time: %f", "", min_which, min_b->a[min_b->i].what, min_b->a[min_b->i].timestamp, - min_b->a[min_b->i].fid); + min_b->a[min_b->i].fid, + min_b->a[min_b->i].data); --min_b->count; min_b->i++; @@ -1176,7 +1209,7 @@ void scheme_wrong_contract_from_ft(const char *who, const char *expected_type, i scheme_wrong_contract(who, expected_type, what, argc, argv); -static Scheme_Object *make_future(Scheme_Object *lambda, int enqueue) +static Scheme_Object *make_future(Scheme_Object *lambda, int enqueue, future_t *cur_ft) /* Called in runtime thread --- as atomic on behalf of a future thread if `lambda' is known to be a thunk */ { @@ -1225,7 +1258,7 @@ static Scheme_Object *make_future(Scheme_Object *lambda, int enqueue) mzrt_mutex_lock(fs->future_mutex); futureid = ++fs->next_futureid; ft->id = futureid; - record_fevent(FEVENT_CREATE, futureid); + record_fevent_with_data(FEVENT_CREATE, (cur_ft ? cur_ft->id : NO_FUTURE_ID), futureid); if (enqueue) { if (ft->status != PENDING_OVERSIZE) enqueue_future(fs, ft); @@ -1244,18 +1277,24 @@ int scheme_can_apply_native_in_future(Scheme_Object *proc) return (((Scheme_Native_Closure *)proc)->code->max_let_depth < FUTURE_RUNSTACK_SIZE * sizeof(void*)); } -static Scheme_Object *do_make_future(int argc, Scheme_Object *argv[]) +static Scheme_Object *do_make_future(int argc, Scheme_Object *argv[], future_t *cur_ft) { + Scheme_Future_State *fs; scheme_check_proc_arity("future", 0, 0, argc, argv); - return make_future(argv[0], 1); + + fs = scheme_future_state; + flush_future_logs(fs); + + return make_future(argv[0], 1, cur_ft); } Scheme_Object *scheme_future(int argc, Scheme_Object *argv[]) XFORM_SKIP_PROC /* can be called from future thread */ { Scheme_Future_Thread_State *fts = scheme_future_thread_state; - if (fts->is_runtime_thread) - return do_make_future(argc, argv); + if (fts->is_runtime_thread) { + return do_make_future(argc, argv, (scheme_current_thread ? scheme_current_thread->current_ft : NULL)); + } else { Scheme_Object *proc = argv[0]; #ifdef MZ_PRECISE_GC @@ -1267,6 +1306,7 @@ Scheme_Object *scheme_future(int argc, Scheme_Object *argv[]) future_t *ft; ft = MALLOC_ONE_TAGGED(future_t); if (ft) { + future_t *cur_ft = scheme_current_thread->current_ft; Scheme_Future_State *fs = scheme_future_state; ft->so.type = scheme_future_type; @@ -1276,7 +1316,7 @@ Scheme_Object *scheme_future(int argc, Scheme_Object *argv[]) mzrt_mutex_lock(fs->future_mutex); ft->id = ++fs->next_futureid; - record_fevent(FEVENT_CREATE, ft->id); + record_fevent_with_data(FEVENT_CREATE, (cur_ft ? cur_ft->id : NO_FUTURE_ID), ft->id); enqueue_future(fs, ft); mzrt_mutex_unlock(fs->future_mutex); @@ -1301,9 +1341,11 @@ static Scheme_Object *would_be_future(int argc, Scheme_Object *argv[]) /* Called in runtime thread */ { future_t *ft; + Scheme_Future_Thread_State *fts; scheme_check_proc_arity("would-be-future", 0, 0, argc, argv); - - ft = (future_t*)make_future(argv[0], 0); + fts = scheme_future_thread_state; + + ft = (future_t*)make_future(argv[0], 0, (fts->thread ? fts->thread->current_ft : NULL)); ft->in_tracing_mode = 1; ft->fts = scheme_future_thread_state; @@ -1779,11 +1821,15 @@ static Scheme_Object *shallower_apply_future_lw_k(void) static int future_in_runtime(Scheme_Future_State *fs, future_t * volatile ft, int what) { mz_jmp_buf newbuf, * volatile savebuf; - Scheme_Thread *p = scheme_current_thread; + Scheme_Thread *p = scheme_current_thread; Scheme_Object * volatile retval; future_t * volatile old_ft; int done; + //FUTURE_ASSERT((!scheme_future_thread_state && !p->current_ft) || scheme_future_thread_state); + //FUTURE_ASSERT(scheme_future_thread_state->thread == p); + //FUTURE_ASSERT(scheme_future_thread_state->thread->current_ft == p->current_ft); + old_ft = p->current_ft; p->current_ft = ft; @@ -1990,17 +2036,20 @@ Scheme_Object *touch(int argc, Scheme_Object *argv[]) Scheme_Future_Thread_State *fts = scheme_future_thread_state; if (fts->is_runtime_thread) { future_t *ft; + future_t *targ_ft; if (fts->thread && (ft = fts->thread->current_ft) && ft->in_tracing_mode) { + targ_ft = (future_t*)argv[0]; Scheme_Future_State *fs = scheme_future_state; - log_future_event( fs, - "future %d, process %d: %s: %s; time: %f", - "touch", - -1, - FEVENT_RTCALL_TOUCH, - get_future_timestamp(), - ft->id); + log_future_event_with_data( fs, + "future %d, process %d: %s: %s; time: %f", + "touch", + -1, + FEVENT_RTCALL_TOUCH, + get_future_timestamp(), + ft->id, + targ_ft->id); } return general_touch(argc, argv); @@ -2658,6 +2707,8 @@ static void future_do_runtimecall(Scheme_Future_Thread_State *fts, runtime thread so we can log all of its primitive applications). */ { future_t *future; + future_t *targ_future; + Scheme_Object **prim_argv; Scheme_Future_State *fs = scheme_future_state; void *storage[4]; int insist_to_suspend, prefer_to_suspend, fid; @@ -2836,6 +2887,7 @@ static void future_do_runtimecall(Scheme_Future_Thread_State *fts, scheme_future_longjmp(*scheme_current_thread->error_buf, 1); } else { FUTURE_ASSERT(future->status == RUNNING); + record_fevent(FEVENT_START_WORK, fid); } } @@ -3270,14 +3322,25 @@ static void do_invoke_rtcall(Scheme_Future_State *fs, future_t *future) flush_future_logs(fs); - /* use log_future_event so we can include `str' in the message: */ - log_future_event(fs, - "future %d, process %d: %s: %s; time: %f", - src, - -1, - (future->rt_prim_is_atomic ? FEVENT_HANDLE_RTCALL_ATOMIC : FEVENT_HANDLE_RTCALL), - get_future_timestamp(), - future->id); + /* use lg_future_event so we can include `str' in the message: */ + if (future->prim_protocol == SIG_ALLOC) { + log_future_event_with_data(fs, + "future %d, process %d: %s: %s; time: %f", + src, + -1, + (future->rt_prim_is_atomic ? FEVENT_HANDLE_RTCALL_ATOMIC : FEVENT_HANDLE_RTCALL), + get_future_timestamp(), + future->id, + future->arg_i0); + } else { + log_future_event(fs, + "future %d, process %d: %s: %s; time: %f", + src, + -1, + (future->rt_prim_is_atomic ? FEVENT_HANDLE_RTCALL_ATOMIC : FEVENT_HANDLE_RTCALL), + get_future_timestamp(), + future->id); + } } if (((future->source_type == FSRC_RATOR) @@ -3336,7 +3399,7 @@ static void do_invoke_rtcall(Scheme_Future_State *fs, future_t *future) { Scheme_Object *s = future->arg_s1; future->arg_s1 = NULL; - s = make_future(s, 1); + s = make_future(s, 1, future); future->retval_s = s; break; } diff --git a/src/racket/src/future.h b/src/racket/src/future.h index 8787100ac3..7895ee122d 100644 --- a/src/racket/src/future.h +++ b/src/racket/src/future.h @@ -45,7 +45,7 @@ typedef void* (*prim_pvoid_pvoid_pvoid_t)(void*, void*); typedef struct Fevent { double timestamp; - int what, fid; + int what, fid, data; } Fevent; typedef struct Fevent_Buffer {