diff --git a/Makefile b/Makefile index 4577ca3..307c725 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ __ignored__ := $(shell ./setup.sh) -PACKAGES=syndicate syndicate-examples +PACKAGES=syndicate syndicate-examples syndicate-msd COLLECTS=syndicate syndicate-examples all: setup diff --git a/syndicate-msd/info.rkt b/syndicate-msd/info.rkt new file mode 100644 index 0000000..960a657 --- /dev/null +++ b/syndicate-msd/info.rkt @@ -0,0 +1,36 @@ +;;; SPDX-License-Identifier: LGPL-3.0-or-later +;;; SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones + +#lang setup/infotab +(define collection 'multi) + +(define deps '( + + "base" + + "syndicate" + "preserves" + + "struct-defaults" + + ;; "bitsyntax" + ;; "packet-socket" + ;; "compatibility-lib" + ;; "data-lib" + ;; "gui-lib" + ;; "htdp-lib" + ;; "images-lib" + ;; "net-lib" + ;; "pict-lib" + ;; "plot-lib" + ;; "srfi-lite-lib" + + )) + +;; (define build-deps '("rackunit-lib")) + +(define racket-launcher-names + '("syndicate-render-msd")) + +(define racket-launcher-libraries + '("syndicate/msd/bin/syndicate-render-msd.rkt")) diff --git a/syndicate-msd/syndicate/msd/bin/syndicate-render-msd.rkt b/syndicate-msd/syndicate/msd/bin/syndicate-render-msd.rkt new file mode 100644 index 0000000..4ecadb0 --- /dev/null +++ b/syndicate-msd/syndicate/msd/bin/syndicate-render-msd.rkt @@ -0,0 +1,50 @@ +#lang racket +;;; SPDX-License-Identifier: LGPL-3.0-or-later +;;; SPDX-FileCopyrightText: Copyright © 2017-2022 Tony Garnock-Jones + +(module+ main + (require racket/cmdline) + (require "../types.rkt") + (require "../read.rkt") + (require "../render.rkt") + + (define config (render-config)) + (define *target* 'screen) + + (define-syntax-rule (! [fld val] ...) + (set! config (struct-copy config [fld val] ...))) + + (define filename + (command-line + #:once-each + [("-s" "--scale") scale "Rescales output; 1 = 100%" + (! [scale (string->number scale)])] + [("-t" "--target") target "Choose target: screen, png, png@2x, svg, eps, pdf" + (set! *target* (string->symbol target))] + [("--width") width ((format "Width of swimlane cells (default: ~a)" + (render-config-width config))) + (! [width (string->number width)])] + [("--height") height ((format "Minimum height of rows (default: ~a)" + (render-config-width config))) + (! [height (string->number height)])] + [("--gap") gap ((format "Extra space between rows (default: ~a)" (render-config-gap config))) + (! [gap (string->number gap)])] + [("--number") "Enables step numbering" + (! [number-steps? #t])] + [("--no-number") "Disables step numbering" + (! [number-steps? #f])] + [("--number-gap") number-gap ((format "Extra space to row numbers (default: ~a)" + (render-config-number-gap config))) + (! [number-gap (string->number number-gap)])] + #:args (filename) + filename)) + + (define (read-input port) + (read-msd port #:config config)) + + (define msd (if (equal? filename "-") + (read-input (current-input-port)) + (call-with-input-file filename read-input))) + + (render (msd->pict msd #:config config) + #:target *target*)) diff --git a/syndicate-msd/syndicate/msd/read.rkt b/syndicate-msd/syndicate/msd/read.rkt new file mode 100644 index 0000000..0f6c7b3 --- /dev/null +++ b/syndicate-msd/syndicate/msd/read.rkt @@ -0,0 +1,220 @@ +#lang racket/base +;;; SPDX-License-Identifier: LGPL-3.0-or-later +;;; SPDX-FileCopyrightText: Copyright © 2017-2022 Tony Garnock-Jones + +(provide read-msd) + +(require (only-in racket/format ~a ~r)) +(require (only-in racket/list flatten)) +(require racket/match) +(require racket/set) + +(require preserves) +(require preserves-schema/methods) +(require syndicate/schemas/trace) +(require (prefix-in P: syndicate/schemas/protocol)) +(require "types.rkt") + +(define (read-msd port #:config [config (render-config)]) + (define max-lane -1) + (define logical-time 1) + (define swimlane-map (make-hash)) + (define turn-map (make-hash)) + (define handle-map (make-hash)) + (define events-rev '()) + + (define (emit! e) + (when e + (set! logical-time (+ logical-time 1)) + (set! events-rev (cons e events-rev)))) + + (define (find-unused-lane swimlane-map) + (let ((used-lanes (list->set (hash-values swimlane-map)))) + (do ((i 0 (+ i 1))) + ((not (set-member? used-lanes i)) i)))) + + (define (translate! actor-id [moment logical-time]) + (define new? #f) + (define p (diagram-position (hash-ref! swimlane-map + actor-id + (lambda () + (define lane (find-unused-lane swimlane-map)) + (when (> lane max-lane) (set! max-lane lane)) + (set! new? #t) + lane)) + moment)) + (when new? (emit! (begin-swimlane p))) + p) + + (define (vacate-lane! actor-id) + (hash-remove! swimlane-map actor-id)) + + (define (turn-actor turn-id) + (car (hash-ref turn-map turn-id))) + + (define (turn-moment turn-id) + (cdr (hash-ref turn-map turn-id))) + + (define (connection* #:bidirectional? [bidirectional? #f] source-actor-id sink-actor-id) + (and source-actor-id + (connection bidirectional? + (translate! source-actor-id) + (translate! sink-actor-id)))) + + (define (format-turn-label actor-id turn-id turn-cause) + (define sub-label + (match turn-cause + [(TurnCause-turn other) + (emit! (connection #f + (translate! (turn-actor other) (turn-moment other)) + (translate! actor-id))) + (format "caused by ~a" (~turn other))] + [(TurnCause-cleanup) + "cleanup"] + [(TurnCause-linkedTaskRelease task reason) + (format "task ~a ended: ~a" task (->preserve reason))] + [(TurnCause-periodicActivation period) + (format "timer: every ~as" period)] + [(TurnCause-delay other-turn delay-amount) + (emit! (connection* (turn-actor other-turn) actor-id)) + (format "delay: ~as" delay-amount)] + [(TurnCause-external desc) + (format "external: ~a" desc)])) + (format "~a: ~a" (~turn turn-id) sub-label)) + + (define (~actor-id a) + (~a (ActorId-value a))) + + (define (~turn t) + (format "#~a" (TurnId-value t))) + + (define (~target e) + (match-define (Target actor-id facet-id oid) e) + (format "~a/~a:~a" + (~actor-id actor-id) + (FacetId-value facet-id) + (let ((oid (Oid-value oid))) + (if (number? oid) + (~r oid #:base 16 #:min-width 16 #:pad-string "0") + oid)))) + + (define (~handle h) + (format "H~a" (P:Handle-value h))) + + (define (~adesc adesc) + (match adesc + [(AssertionDescription-value v) (preserve->string v #:encode-embedded values #:commas? #f)] + [(AssertionDescription-opaque d) (format "⌜~a⌝" d)])) + + (define (~turn-event te is-action?) + (match te + [(TurnEvent-assert adesc handle) + (when is-action? + (when (hash-has-key? handle-map handle) + (log-error "Duplicate use of handle ~a" (~handle handle))) + (hash-set! handle-map handle adesc)) + (format "assert ~a: ~a" (~handle handle) (~adesc adesc))] + [(TurnEvent-retract handle) + (define adesc (hash-ref handle-map handle #f)) + (format "retract ~a: ~a" (~handle handle) (if adesc (~adesc adesc) "(missing)"))] + [(TurnEvent-message adesc) + (format "message ~a" (~adesc adesc))] + [(TurnEvent-sync peer) + (format "sync ~a" (~target peer))] + [(TurnEvent-breakLink source-actor-id handle) + (format "break link from ~a" (~actor-id source-actor-id))])) + + (define (~targeted-turn-event tte is-action?) + (match-define (TargetedTurnEvent target detail) tte) + (format "~a <-- ~a" (~target target) (~turn-event detail is-action?))) + + (define (process-action! actor-id action) + (let-values + (((variation label) + (match action + [(ActionDescription-dequeue tte) + (values 'event (~targeted-turn-event tte #f))] + [(ActionDescription-enqueue tte) + (values 'action (~targeted-turn-event tte #t))] + [(ActionDescription-dequeueInternal tte) + (values 'event (format "(internal) ~a" (~targeted-turn-event tte #f)))] + [(ActionDescription-enqueueInternal tte) + (values 'action (format "(internal) ~a" (~targeted-turn-event tte #t)))] + [(ActionDescription-spawn linked? child-actor-id) + (translate! child-actor-id) ;; allocates swimlane + (define L (annotate-swimlane (translate! actor-id) + (render-config-action-color config) + (format "spawn~a ~a" (if linked? "/link" "") (~actor-id child-actor-id)))) + (define C (and (not linked?) (connection* actor-id child-actor-id))) + (emit! L) + (when C (emit! C)) + (values #f #f)] + [(ActionDescription-link parent-actor-id handle-child->parent child-actor-id handle-parent->child) + (define L1 (annotate-swimlane (translate! parent-actor-id) + (render-config-action-color config) + (format "linked to ~a" (~actor-id child-actor-id)))) + (define L2 (annotate-swimlane (translate! child-actor-id) + (render-config-action-color config) + (format "linked to ~a" (~actor-id parent-actor-id)))) + (define C (connection* #:bidirectional? #t parent-actor-id child-actor-id)) + (emit! L1) + (emit! L2) + (emit! C) + (values #f #f)] + [(ActionDescription-facetStart facet-ids) + (values 'action (format "+facet ~a" (map FacetId-value facet-ids)))] + [(ActionDescription-facetStop facet-ids reason) + (values 'action (format "-facet ~a ~a" (map FacetId-value facet-ids) (->preserve reason)))] + [(ActionDescription-linkedTaskStart name task-id) + (values 'action (format "task ~a started: ~a" task-id name))]))) + (when label + (emit! (annotate-swimlane (translate! actor-id) + (match variation + ['event (render-config-event-color config)] + ['action (render-config-action-color config)]) + label))))) + + (define (process-trace-entry! entry) + (match-define (TraceEntry _timestamp actor-id activation) entry) + (match activation + [(ActorActivation-start name) + (emit! (annotate-swimlane (translate! actor-id) + (render-config-label-color config) + (match name + [(Name-anonymous) "(anonymous)"] + [(Name-named n) (preserve->string n + #:encode-embedded values + #:indent 2 + #:commas? #f)])))] + [(ActorActivation-turn (TurnDescription turn-id turn-cause actions)) + (emit! (activate-swimlane (translate! actor-id))) + (let ((label (format-turn-label actor-id turn-id turn-cause))) + (emit! (annotate-swimlane (translate! actor-id) (render-config-event-color config) label))) + (for [(a (in-list actions))] (process-action! actor-id a)) + ;; (let ((action-count (length actions))) + ;; (when (positive? action-count) + ;; (emit! (annotate-swimlane (translate! actor-id) + ;; (render-config-action-color config) + ;; (match action-count + ;; [1 "1 action"] + ;; [n (format "~a actions" n)]))))) + (hash-set! turn-map turn-id (cons actor-id logical-time)) + (emit! (deactivate-swimlane (translate! actor-id))) + ] + [(ActorActivation-stop status) + (emit! (end-swimlane (translate! actor-id))) + (when (ExitStatus-Error? status) + (emit! (annotate-swimlane (translate! actor-id) + (render-config-label-color config) + (format "crashed: ~a" (->preserve status))))) + (vacate-lane! actor-id)])) + + (let loop () + (match (read-preserve port #:decode-embedded values) + [(? eof-object?) + (msd max-lane (reverse events-rev))] + [input + (match (parse-TraceEntry input) + [(? eof-object?) (error 'read-msd "Invalid trace entry: ~v" input)] + [entry (process-trace-entry! entry)]) + (loop)]))) diff --git a/syndicate-msd/syndicate/msd/render.rkt b/syndicate-msd/syndicate/msd/render.rkt new file mode 100644 index 0000000..cdf2890 --- /dev/null +++ b/syndicate-msd/syndicate/msd/render.rkt @@ -0,0 +1,268 @@ +#lang racket/base +;;; SPDX-License-Identifier: LGPL-3.0-or-later +;;; SPDX-FileCopyrightText: Copyright © 2017-2022 Tony Garnock-Jones + +(provide msd->pict + render) + +(require (only-in racket/list partition last)) +(require racket/match) +(require racket/math) +(require racket/set) +(require (only-in racket/string string-split)) + +(require preserves) + +(require pict) +(require pict/color) +(require file/convertible) + +(require "types.rkt") + +;;--------------------------------------------------------------------------- + +(define N (* pi 1/2)) +(define S (* pi -1/2)) +(define E (* pi 0)) +(define W (* pi 1)) + +(define NE (* pi 1/4)) +(define SE (* pi -1/4)) +(define NW (* pi 3/4)) +(define SW (* pi -3/4)) + +(define ENE (* pi 1/8)) +(define ESE (* pi -1/8)) +(define WNW (* pi 7/8)) +(define WSW (* pi -7/8)) + +;; A SwimlaneState is +;; - a (labelled-cell String ColorString SwimlaneState), representing +;; a fresh annotation on a possibly-new swimlane +;; - 'inactive, an occupied but inactive lane +;; - 'active, an occupied and active lane +;; - #f, an unoccupied lane + +(struct labelled-cell (label color underlying-state) #:prefab) + +(define (hash-set-or-remove h k v) + (if v + (hash-set h k v) + (hash-remove h k))) + +(define (reset-label v) + (match v + [(labelled-cell _ _ underlying-state) underlying-state] + [_ v])) + +(define (reset-statemap statemap) + (for/hash [((k v) (in-hash statemap))] + (values k (reset-label v)))) + +(define (transpose xs) + (apply map list xs)) + +(define (FR color w h) + (filled-rectangle #:color color #:draw-border? #f w h)) + +(define (should-label-row? row-number max-lane current-row) + (and (not (negative? row-number)) + (for/or [(lane (+ max-lane 1))] + (labelled-cell? (hash-ref current-row lane #f))))) + +(define (msd->pict m #:config [config (render-config)]) + (match-define (msd max-lane all-events) m) + (define-values (connections events) (partition connection? all-events)) + + (define-values (_row-count statemaps) + (for/fold [(previous-row 0) (table (hash))] + [(event (in-list events))] + (define-values (pos new-state) + (match event + [(begin-swimlane pos) + (values pos 'inactive)] + [(activate-swimlane pos) + (values pos 'active)] + [(deactivate-swimlane pos) + (values pos 'inactive)] + [(end-swimlane pos) + (values pos #f)] + [(annotate-swimlane pos color annotation) + (values pos (list color annotation))])) + + (match-define (diagram-position lane row) pos) + + (define old-statemap + (hash-ref table row (lambda () (reset-statemap (hash-ref table previous-row hash))))) + + (define new-statemap + (hash-set-or-remove old-statemap + lane + (match new-state + [(list color annotation) + (labelled-cell annotation color (hash-ref old-statemap lane 'inactive))] + [_ new-state]))) + + (values row (hash-set-or-remove table row (if (hash-empty? new-statemap) #f new-statemap))))) + + (define (fill-out height p) + (ct-superimpose (blank (render-config-width config) height) + (if p + (inset p (* -1/2 (pict-width p)) 0) + (blank 0)))) + + (define (inactive height) + (FR "gray" (/ (render-config-active-width config) 2) height)) + + (define (render-underlay max-lane prev-row current-row next-row height) + (for/list [(lane (+ max-lane 1))] + (define prev-state (hash-ref prev-row lane #f)) + (define next-state (hash-ref next-row lane #f)) + (fill-out height + (match (match (hash-ref current-row lane #f) + [(labelled-cell s _ u) u] + [u u]) + ['inactive (inactive height)] + ['active + (define (trunk height) + (hb-append 0 + (FR "black" 1 height) + (FR (render-config-activation-color config) (- (render-config-active-width config) 2) height) + (FR "black" 1 height))) + (define bar (FR "black" (render-config-active-width config) 1)) + (match* (prev-state next-state) + [('active 'active) (trunk height)] + [('active _) (vl-append 0 (trunk (- height 1)) bar)] + [(_ 'active) (vl-append 0 bar (trunk (- height 1)))] + [(_ _) (vl-append 0 bar (trunk (- height 2)) bar)])] + [#f (blank 0)])))) + + (define (render-overlay max-lane current-row) + (for/list [(lane (+ max-lane 1))] + (fill-out (render-config-height config) + (match (hash-ref current-row lane #f) + [(labelled-cell s color u) + (define para (apply vl-append 0 + (map (lambda (s) + (define limit 200) + (text (if (> (string-length s) limit) + (string-append (substring s 0 limit) "...") + s) + 'modern)) + (string-split s "\n")))) + (vc-append + (disk 4) + (frame + (cc-superimpose + (FR color (+ (pict-width para) 8) (+ (pict-height para) 8)) + para)))] + [_ #f])))) + + (define rows (sort (hash->list statemaps) < #:key car)) + + (define dummy-initial-row (cons (- (car (car rows)) 1) (hash))) + (define dummy-final-row (match (last rows) + [(cons final-row final-statemap) + (cons (+ final-row 1) (reset-statemap final-statemap))])) + (define row-triples + (map (lambda (a b c) + (list (car b) + (reset-statemap (cdr a)) + (cdr b) + (reset-statemap (cdr c)))) + (cons dummy-initial-row rows) + (append rows (list dummy-final-row)) + (append (cdr rows) (list dummy-final-row dummy-final-row)))) + + (define over-and-unders + (map (match-lambda + [(list row-number prev-row current-row next-row) + (define overlay (render-overlay max-lane current-row)) + (define height (+ (render-config-gap config) (apply max (map pict-height overlay)))) + (define underlay (render-underlay max-lane prev-row current-row next-row height)) + (list row-number + underlay + overlay + (should-label-row? row-number max-lane current-row))]) + row-triples)) + + (define overlay-index (for/hash [(entry over-and-unders)] + (values (car entry) (caddr entry)))) + + (define base-pict + (apply vl-append 0 + (map (lambda (over-and-under) + (hc-append (ct-superimpose (apply hb-append 4 (cadr over-and-under)) + (apply hb-append 4 (caddr over-and-under))) + (if (and (render-config-number-steps? config) (cadddr over-and-under)) + (let ((L (cc-superimpose + (circle 30) + (text (number->string (car over-and-under)))))) + (hc-append (blank (render-config-number-gap config) 0) + (inset L + (- (pict-width L)) + (- (pict-height L))))) + (blank 0)))) + over-and-unders))) + + (define (overlay-pict lane row) + (define picts (hash-ref overlay-index row '())) + (and (< lane (length picts)) (list-ref picts lane))) + + (define (find-overlay lane row) + (or (for/or [(r (in-naturals row))] (overlay-pict lane r)) + (for/or [(r (in-range (- row 1) -1 -1))] (overlay-pict lane r)))) + + (define unscaled-pict + (for/fold [(base-pict base-pict)] + [(c connections)] + (match-define (connection bidirectional? + (diagram-position from-lane from-row) + (diagram-position to-lane to-row)) + c) + (define from-pict (find-overlay from-lane from-row)) + (define to-pict (find-overlay to-lane to-row)) + (if (and from-pict to-pict) + (if bidirectional? + (pin-arrows-line 10 base-pict + from-pict + (if (>= to-lane from-lane) rc-find lc-find) + to-pict + (if (>= to-lane from-lane) lc-find rc-find) + #:start-angle (if (>= to-lane from-lane) E W) + #:start-pull 1/10 + #:end-angle (if (<= to-lane from-lane) W E) + #:end-pull 1/10) + (pin-arrow-line 10 base-pict + from-pict + cb-find + to-pict + ct-find + #:start-angle (if (>= to-lane from-lane) SE SW) + #:start-pull 1/10 + #:end-angle (if (<= to-lane from-lane) SW SE) + #:end-pull 1/10)) + base-pict))) + + (scale unscaled-pict (render-config-scale config))) + +(define (render p #:target target) + (define (final-border) 1) + (define final-pict (cc-superimpose (blank (+ (pict-width p) (* 2 (final-border))) + (+ (pict-height p) (* 2 (final-border)))) + (panorama p))) + (case target + [(screen) + (local-require racket/gui/base) + (show-pict final-pict 800 600) + (void)] + [(png) + (display (convert final-pict 'png-bytes))] + [(png@2x) + (display (convert final-pict 'png@2x-bytes))] + [(svg) + (display (convert final-pict 'svg-bytes))] + [(eps) + (display (convert final-pict 'eps-bytes))] + [(pdf) + (display (convert final-pict 'pdf-bytes))])) diff --git a/syndicate-msd/syndicate/msd/types.rkt b/syndicate-msd/syndicate/msd/types.rkt new file mode 100644 index 0000000..a48a214 --- /dev/null +++ b/syndicate-msd/syndicate/msd/types.rkt @@ -0,0 +1,59 @@ +#lang racket/base +;;; SPDX-License-Identifier: LGPL-3.0-or-later +;;; SPDX-FileCopyrightText: Copyright © 2017-2022 Tony Garnock-Jones + +(provide (except-out (all-defined-out) render-config) + (rename-out [make-render-config render-config] + [render-config ])) + +(require struct-defaults) + +;;--------------------------------------------------------------------------- +;; Representation of a parsed trace. + +(struct msd (max-lane events) #:prefab) + +(struct diagram-position (lane row) #:prefab) + +(struct begin-swimlane (pos) #:prefab) +(struct activate-swimlane (pos) #:prefab) +(struct deactivate-swimlane (pos) #:prefab) +(struct end-swimlane (pos) #:prefab) +(struct annotate-swimlane (pos color annotation) #:prefab) +(struct connection (bidirectional? from-pos to-pos) #:prefab) + +;;--------------------------------------------------------------------------- +;; Configuration for the renderer + +(struct render-config ( + width + height + gap + number-steps? + number-gap + active-width + activation-color + action-color + event-color + label-color + scale + ) #:prefab) + +(define-struct-defaults make-render-config render-config + [ + #:width [render-config-width 100] + #:height [render-config-height 4] + #:gap [render-config-gap 12] + + #:number-steps? [render-config-number-steps? #f] + #:number-gap [render-config-number-gap 0] + + #:active-width [render-config-active-width 10] + + #:activation-color [render-config-activation-color "pink"] + #:action-color [render-config-action-color "white"] + #:event-color [render-config-event-color "orange"] + #:label-color [render-config-label-color "palegreen"] + + #:scale [render-config-scale 1] + ])