diff --git a/.travis.yml b/.travis.yml index 9448f4ed..829f68f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,7 +34,7 @@ before_install: install: - - raco pkg install + - raco pkg install --auto script: - time raco make test/all-rosette-tests.rkt diff --git a/NOTES.md b/NOTES.md index 7a0be8b0..f17983d4 100644 --- a/NOTES.md +++ b/NOTES.md @@ -11,9 +11,11 @@ The semantics of Rosette 3.0 differs from Rosette 2.x in two ways: This release also includes the following new functionality and features contributed by [James Bornholt][] and [Phitchaya Mangpo Phothilimthana][]: +- Developed a new *symbolic profiler* for diagnosing performance issues in Rosette programs. The symbolic profiler instruments Rosette and tracks key performance metrics to identify potential issues. To use the symbolic profiler, run the command `raco symprofile program.rkt`. A new [performance][] chapter in the Rosette guide details common performance issues and how to use the symbolic profiler to identify them. - Extended and generalized the interface to constraint solvers. The new interface allows the client code to specify a path to the solver, set the logic, provide solver-specific configuration options, and export the problem encodings sent to the solver. - Added support for four new solvers: [Boolector][], [CVC4][], [Yices][], and [CPLEX][]. These solvers are not included in the default distribution and need to be installed separately for use with Rosette. +[performance]: https://docs.racket-lang.org/rosette-guide/ch_performance.html [Boolector]: https://docs.racket-lang.org/rosette-guide/sec_solvers-and-solutions.html#%28def._%28%28lib._rosette%2Fsolver%2Fsmt%2Fboolector..rkt%29._boolector%29%29 [CVC4]: https://docs.racket-lang.org/rosette-guide/sec_solvers-and-solutions.html#%28def._%28%28lib._rosette%2Fsolver%2Fsmt%2Fcvc4..rkt%29._cvc4%29%29 [Yices]: https://docs.racket-lang.org/rosette-guide/sec_solvers-and-solutions.html#%28def._%28%28lib._rosette%2Fsolver%2Fsmt%2Fyices..rkt%29._yices%29%29 diff --git a/info.rkt b/info.rkt index 063fef11..8452bd90 100644 --- a/info.rkt +++ b/info.rkt @@ -3,13 +3,14 @@ (define collection 'multi) (define deps '("r6rs-lib" + "rfc6455" "rackunit-lib" "slideshow-lib" "base")) (define build-deps '("pict-doc" "scribble-lib" - "racket-doc")) + "racket-doc")) (define test-omit-paths (if (getenv "PLT_PKG_BUILD_SERVICE") 'all '())) diff --git a/rosette/base/core/lift.rkt b/rosette/base/core/lift.rkt index d2343034..d6901ae3 100644 --- a/rosette/base/core/lift.rkt +++ b/rosette/base/core/lift.rkt @@ -52,12 +52,12 @@ (define-syntax (define/lift stx) (syntax-case stx (: :: ->) [(_ (id0 id ...) :: contracted? -> rosette-type?) - (or (identifier? #'contracted) (raise-argument-error "identifier?" #'contracted?)) + (or (identifier? #'contracted?) (raise-argument-error "identifier?" #'contracted?)) #'(begin (define/lift id0 :: contracted? -> rosette-type?) (define/lift id :: contracted? -> rosette-type?) ...)] [(_ id :: contracted? -> rosette-type?) ; repeated from (_ id : contracted? -> rosette-type?) - params don't work - (or (identifier? #'contracted) (raise-argument-error "identifier?" #'contracted?)) + (or (identifier? #'contracted?) (raise-argument-error "identifier?" #'contracted?)) #`(define (#,(lift-id #'id) val) (if (contracted? val) (id val) diff --git a/rosette/base/core/merge.rkt b/rosette/base/core/merge.rkt index 73ea7e30..856049d5 100644 --- a/rosette/base/core/merge.rkt +++ b/rosette/base/core/merge.rkt @@ -2,7 +2,7 @@ (require (only-in rnrs/base-6 assert) (only-in racket/unsafe/ops [unsafe-car car] [unsafe-cdr cdr]) - "term.rkt" "union.rkt" "bool.rkt") + "term.rkt" "union.rkt" "bool.rkt" "reporter.rkt") (provide merge merge* unsafe-merge* merge-same) @@ -32,10 +32,12 @@ (do-merge* #t ps)) (define-syntax-rule (do-merge* force? ps) - (match (compress force? (simplify ps)) - [(list (cons g v)) (assert (not (false? g))) v] - [(list _ (... ...) (cons #t v) _ (... ...)) v] - [vs (apply union vs)])) + (let ([simp (simplify ps)]) + ((current-reporter) 'merge (length simp)) + (match (compress force? simp) + [(list (cons g v)) (assert (not (false? g))) v] + [(list _ (... ...) (cons #t v) _ (... ...)) v] + [vs (apply union vs)]))) (define (guard-&& a b) (match b diff --git a/rosette/base/core/reporter.rkt b/rosette/base/core/reporter.rkt new file mode 100644 index 00000000..f95a18a2 --- /dev/null +++ b/rosette/base/core/reporter.rkt @@ -0,0 +1,15 @@ +#lang racket + +(require racket/generic (for-syntax racket/syntax)) +(provide (all-defined-out)) + +; The reporter is called when "interesting" +; events happen during symbolic execution; for example, +; when a merge occurs or a new term is created. +(define current-reporter + (make-parameter + void + (lambda (new-reporter) + (unless (procedure? new-reporter) + (raise-argument-error 'current-reporder "procedure?" new-reporter)) + new-reporter))) diff --git a/rosette/base/core/term.rkt b/rosette/base/core/term.rkt index 57278a33..57f7c885 100644 --- a/rosette/base/core/term.rkt +++ b/rosette/base/core/term.rkt @@ -1,6 +1,7 @@ #lang racket -(require racket/syntax (for-syntax racket racket/syntax) racket/generic "type.rkt") +(require racket/syntax (for-syntax racket racket/syntax) racket/generic + "type.rkt" "reporter.rkt") (provide term-cache clear-terms! @@ -18,7 +19,7 @@ ; of expressions with commutative operators. #|-----------------------------------------------------------------------------------|# (define term-cache (make-parameter (make-hash))) -(define term-count (make-parameter 0)) +(define term-count (make-parameter 0)) ; Clears the entire term-cache if invoked with #f (default), or ; it clears all terms reachable from the given set of leaf terms. @@ -67,11 +68,12 @@ (define (term x 15))) + ((3) + 1 + (((lib "rosette/doc/guide/scribble/util/lifted.rkt") + . + deserialize-info:opaque-v0)) + 0 + () + () + (c values c (0 (u . "(unsat)")))) + #"" + #"") +((define (list-set lst idx val) + (let-values (((front back) (split-at lst idx))) + (append front (cons val (cdr back))))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((list-set '(a b c) 1 'd) ((3) 0 () 0 () () (q values (a d c))) #"" #"") +((define-symbolic* idx len integer?) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define lst (take '(a b c) len)) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((list-set lst idx 'd) + ((3) + 1 + (((lib "rosette/doc/guide/scribble/util/lifted.rkt") + . + deserialize-info:opaque-v0)) + 0 + () + () + (c values c (0 (u . "{2846099531607739527:6}")))) + #"" + #"") +((define (list-set* lst idx val) + (for/all + ((lst lst)) + (map + (lambda (i v) (if (= idx i) val v)) + (build-list (length lst) identity) + lst))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((list-set* '(a b c) 1 'd) ((3) 0 () 0 () () (q values (a d c))) #"" #"") +((list-set* lst idx 'd) + ((3) + 1 + (((lib "rosette/doc/guide/scribble/util/lifted.rkt") + . + deserialize-info:opaque-v0)) + 0 + () + () + (c values c (0 (u . "{-4322348582163789884:4}")))) + #"" + #"") +((define-values (width height) (values 5 5)) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define-symbolic* x y integer?) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define grid/2d (for/vector ((_ height)) (make-vector width #f))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((vector-set! (vector-ref grid/2d y) x 'a) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define grid/flat (make-vector (* width height) #f)) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((vector-set! grid/flat (+ (* y width) x) 'a) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define (maybe-ref lst idx) (if (<= 0 idx 1) (list-ref lst idx) -1)) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define-symbolic* idx integer?) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((maybe-ref '(5 6 7) idx) + ((3) + 1 + (((lib "rosette/doc/guide/scribble/util/lifted.rkt") + . + deserialize-info:opaque-v0)) + 0 + () + () + (c + values + c + (0 + (u + . + "(ite (&& (<= 0 idx$0) (<= idx$0 1)) (ite* (⊢ (= 0 idx$0) 5) (⊢ (= 1 idx$0) 6) (⊢ (= 2 idx$0) 7)) -1)")))) + #"" + #"") +((define (maybe-ref* lst idx) + (cond ((= idx 0) (list-ref lst 0)) ((= idx 1) (list-ref lst 1)) (else -1))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((maybe-ref* '(5 6 7) idx) + ((3) + 1 + (((lib "rosette/doc/guide/scribble/util/lifted.rkt") + . + deserialize-info:opaque-v0)) + 0 + () + () + (c values c (0 (u . "(ite (= 0 idx$0) 5 (ite (= 1 idx$0) 6 -1))")))) + #"" + #"") +((define-values (Add Sub Sqr Nop) (values (bv 0 2) (bv 1 2) (bv 2 2) (bv 3 2))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define (calculate prog (acc (bv 0 4))) + (cond + ((null? prog) acc) + (else + (define ins (car prog)) + (define op (car ins)) + (calculate + (cdr prog) + (cond + ((eq? op Add) (bvadd acc (cadr ins))) + ((eq? op Sub) (bvsub acc (cadr ins))) + ((eq? op Sqr) (bvmul acc acc)) + (else acc)))))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define (list-set lst idx val) + (match + lst + ((cons x xs) + (if (= idx 0) (cons val xs) (cons x (list-set xs (- idx 1) val)))) + (_ lst))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define (sub->add prog idx) + (define ins (list-ref prog idx)) + (if (eq? (car ins) Sub) + (list-set prog idx (list Add (bvneg (cadr ins)))) + prog)) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((define (verify-xform xform N) + (define P + (for/list + ((i N)) + (define-symbolic* op (bitvector 2)) + (define-symbolic* arg (bitvector 4)) + (if (eq? op Sqr) (list op) (list op arg)))) + (define-symbolic* acc (bitvector 4)) + (define-symbolic* idx integer?) + (define xP (xform P idx)) + (verify (assert (eq? (calculate P acc) (calculate xP acc))))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((verify-xform sub->add 5) + ((3) + 1 + (((lib "rosette/doc/guide/scribble/util/lifted.rkt") + . + deserialize-info:opaque-v0)) + 0 + () + () + (c values c (0 (u . "(unsat)")))) + #"" + #"") +((clear-asserts!) ((3) 0 () 0 () () (c values c (void))) #"" #"") +((clear-terms!) ((3) 0 () 0 () () (c values c (void))) #"" #"") +((define-symbolic* idx integer?) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((list-set '(1 2 3) idx 4) + ((3) + 1 + (((lib "rosette/doc/guide/scribble/util/lifted.rkt") + . + deserialize-info:opaque-v0)) + 0 + () + () + (c + values + c + (c + (0 (u . "(ite (= 0 idx$0) 4 1)")) + c + (0 (u . "(ite (= 0 idx$0) 2 (ite (= 0 (+ -1 idx$0)) 4 2))")) + c + (0 + (u + . + "(ite (= 0 idx$0) 3 (ite (= 0 (+ -1 idx$0)) 3 (ite (= 0 (+ -2 idx$0)) 4 3)))"))))) + #"" + #"") +((define (list-set* lst idx val) + (match + lst + ((cons x xs) (cons (if (= idx 0) val x) (list-set* xs (- idx 1) val))) + (_ lst))) + ((3) 0 () 0 () () (c values c (void))) + #"" + #"") +((list-set* '(1 2 3) idx 4) + ((3) + 1 + (((lib "rosette/doc/guide/scribble/util/lifted.rkt") + . + deserialize-info:opaque-v0)) + 0 + () + () + (c + values + c + (c + (0 (u . "(ite (= 0 idx$0) 4 1)")) + c + (0 (u . "(ite (= 0 (+ -1 idx$0)) 4 2)")) + c + (0 (u . "(ite (= 0 (+ -2 idx$0)) 4 3)"))))) + #"" + #"") diff --git a/rosette/doc/guide/scribble/performance/performance.scrbl b/rosette/doc/guide/scribble/performance/performance.scrbl new file mode 100644 index 00000000..330086a4 --- /dev/null +++ b/rosette/doc/guide/scribble/performance/performance.scrbl @@ -0,0 +1,424 @@ +#lang scribble/manual + +@(require scribble/core scribble/html-properties + scribble/bnf scribble/eval + (for-label (except-in racket list-set) errortrace + rosette/base/core/term + rosette/base/form/define + rosette/query/form + rosette/base/core/union + (only-in rosette/query/finitize current-bitwidth) + (only-in rosette/base/base bitvector) + (only-in rosette/base/core/safe assert) + (only-in rosette/base/core/forall for/all)) + racket/runtime-path + "../util/lifted.rkt" + (only-in "../refs.scrbl" ~cite sympro:oopsla18)) + +@(define-runtime-path root ".") +@(define rosette-eval (rosette-log-evaluator (logfile root "performance-log") #f 'rosette)) + +@(define-runtime-path profile.png "profile.png") +@(define-runtime-path profile-xform.png "profile-xform.png") + +@title[#:tag "ch:performance"]{Performance} + +Rosette provides an efficient, general-purpose runtime for solver-aided programming. +But as with any other form of programming, +scaling this runtime to challenging problems involves careful design, +and sometimes requires changes to code to better suit Rosette's +symbolic virtual machine (SVM) style of execution. +This chapter describes common performance problems and solutions, +as well as tools built into Rosette for diagnosing these problems. + +@section[#:tag "sec:antipatterns"]{Common Performance Issues} + +When a Rosette program performs poorly, +it is often due to one of four common issues, described next. + +@subsection{Integer and Real Theories} + +Rosette supports assertions containing symbolic values of integer or real type. +But satisfiability solving with these types is expensive (or, in the worst case, undecidable), +and even simple queries can be unacceptably slow. + +One solution to performance issues with assertions involving integers or reals +is Rosette's @racket[current-bitwidth] parameter, +which controls the @tech{reasoning precision} used for queries. +When @racket[current-bitwidth] is set to a value @emph{k} other than @racket[#f], +Rosette approximates @racket[integer?] and @racket[real?] values using signed @emph{k}-bit @racket[bitvector]s. +This approximation can make assertions involving integers and reals +more efficiently decidable. + +But this approximation is unsound and may produce results that are +incorrect under the infinite-precision semantics of integers and reals +(while being correct under the finite-precision semantics). +For example, this program incorrectly says that no integer greater than 15 exists, +because the setting of @racket[current-bitwidth] causes it to consider only values of @racket{x} +that can be represented as a 5-bit bitvector. + +@interaction[#:eval rosette-eval +(current-bitwidth 5) +(define-symbolic x integer?) +(solve (assert (> x 15)))] + +So, choosing the right reasoning precision for an application involves navigating this tradeoff +between performance and soundness. + +@subsection{Algorithmic Mismatch} + +Small algorithmic changes can have a large impact on the efficiency +of the symbolic evaluation performed by the Rosette SVM. +Often, the most efficient algorithm for symbolic inputs +is different to the most efficient one for concrete inputs---an +@deftech{algorithmic mismatch}. + +For example, +consider this function to set the @tt{idx}th element of a list @tt{lst} to @tt{val}: + +@interaction[#:eval rosette-eval +(define (list-set lst idx val) + (let-values ([(front back) (split-at lst idx)]) + (append front (cons val (cdr back))))) +(list-set '(a b c) 1 'd) +] + +While appropriate for concrete inputs, +this function exhibits poor performance when the +inputs are symbolic: + +@interaction[#:eval rosette-eval +(define-symbolic* idx len integer?) +(define lst (take '(a b c) len)) +(code:line (list-set lst idx 'd) (code:comment "symbolic union with 6 parts")) +] + +The root cause is the @racket[split-at] operation, +which separates the front and back of the list +into different variables. +Because the index @racket[idx] to split at is symbolic, +the Rosette SVM creates two @tech{symbolic unions} to capture +the possible front and back values +as a function of @racket[idx]. +Even though the possible values of @racket[front] and @racket[back] +are related, this separation loses the relationship. + +A better implementation for symbolic inputs +avoids splitting the list by iterating over it, +updating each position depending on whether its index is equal to @tt{idx}: + +@interaction[#:eval rosette-eval +(define (list-set* lst idx val) + (for/all ([lst lst]) + (map (lambda (i v) (if (= idx i) val v)) + (build-list (length lst) identity) + lst))) +(list-set* '(a b c) 1 'd) +(code:line (list-set* lst idx 'd) (code:comment "smaller symbolic union with 4 parts")) +] + +@subsection{Irregular Representation} + +Just as the best algorithm for symbolic inputs can differ +from that for concrete inputs (an @tech{algorithmic mismatch}), +so can the best data structure. +Programming with symbolic values is most efficient +when data structures are regular; +even though an @deftech{irregular representation} may be more space efficient for concrete data, +it can have negative performance impacts when the data is symbolic. + +For example, consider representing a (mutable) 2D grid data structure +using Rosette's lifted support for @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{vectors}. +The obvious representation is to use nested vectors to represent the two dimensions: + +@interaction[#:eval rosette-eval +(define-values (width height) (values 5 5)) +(define-symbolic* x y integer?) +(define grid/2d + (for/vector ([_ height]) + (make-vector width #f))) +(vector-set! (vector-ref grid/2d y) x 'a) +] + +This representation is inefficient when indexed with symbolic values, +because the dereferences are irregular: +the dereference of the @racket[y]-coordinate returns a vector, +whereas the dereference of the @racket[x]-coordinate returns a value. +This irregularity requires Rosette to perform more symbolic evaluation work +to faithfully track the usages of the nested vector. + +An alternative representation stores the entire grid in one vector, +indexed using simple arithmetic: + +@interaction[#:eval rosette-eval +(define grid/flat + (make-vector (* width height) #f)) +(vector-set! grid/flat (+ (* y width) x) 'a) +] + +This variant improves performance by about 2×. + +@subsection{Missed Concretization} + +In addition to employing careful algorithmic and representational choices, +fast solver-aided code provides as much information as possible +about the feasible choices of symbolic values. +Failure to make this information explicit +results in @deftech{missed concretization} opportunities, and +these misses can cause significant performance degradation. + +For example, consider the following toy procedure, +that returns the @racket[idx]th element of the list @racket[lst], +but only if @racket[idx] is 0 or 1: + +@interaction[#:eval rosette-eval +(define (maybe-ref lst idx) + (if (<= 0 idx 1) + (list-ref lst idx) + -1)) +(define-symbolic* idx integer?) +(maybe-ref '(5 6 7) idx) +] + +This procedure has poor performance when given a symbolic index @racket[idx], +because the call to @racket[(list-ref lst idx)] +passes a symbolic index, +but the conditional establishes that the only possible values for that index are 0 or 1. +When the Rosette SVM evaluates the first side of the conditional, +it does not simplify the value of @racket[idx] to be only 0 or 1, +and so the resulting encoding creates infeasible branches +for cases where @racket[idx] is outside that range. +An alternative version captures that concreteness: + +@interaction[#:eval rosette-eval +(define (maybe-ref* lst idx) + (cond [(= idx 0) (list-ref lst 0)] + [(= idx 1) (list-ref lst 1)] + [else -1])) +(maybe-ref* '(5 6 7) idx) +] + +This variant avoids generating infeasible return values +for the cases where @racket[idx] is greater than 1. + +@section[#:tag "sec:sympro"]{Symbolic Profiling} + +Rosette includes a @deftech[#:key "symbolic profiler"]{symbolic profiler} +for diagnosing performance issues in solver-aided programs. +The symbolic profiler instruments key metrics in the Rosette SVM +and relates them to the performance of a Rosette program, +suggesting the locations of potential bottlenecks---that is, parts of +the program that are difficult to evaluate symbolically. +More details about symbolic profiling are available +in the related technical paper @~cite[sympro:oopsla18]. + +@bold{Running the symbolic profiler} has the same limitations as @racketmodname[errortrace]: +before running it, throw away any @filepath{.zo} versions of your program +(i.e., delete any @filepath{compiled} folders in your code's folder hierarchy). + +Then invoke the symbolic profiler on a program file @nonterm{prog} using @exec{raco}: + +@commandline{raco symprofile @nonterm{prog}} + +After executing @nonterm{prog}, +the symbolic profiler produces a browser-based output summarizing the results, +similar to this output: + +@(image profile.png #:scale 0.7) + +The top half of this output visualizes the evolution of the Racket call stack +over time. +Each procedure call is a rectangle in this chart, +with its width corresponding to the total time taken by the call. +Blue highlighted regions reflect @tech{solver} activity +generated by Rosette @seclink["sec:queries"]{queries}. + +The bottom half summarizes important metrics about the SVM on a per-procedure basis. +Each procedure invoked by the program has a row in the table, +with columns that measure: + +@itemlist[ + @item{The total @bold{time} taken by all invocations of the procedure.} + @item{The @bold{term count}, the total number of @tech[#:key "symbolic term"]{symbolic terms} created by the procedure.} + @item{The number of @bold{unused terms}, which are terms created but never sent to a solver.} + @item{The @bold{union size}, counting the total size of all @tech[#:key "symbolic unions"]{symbolic unions} created by the procedure.} + @item{The @bold{merge count}, summing the number of execution paths merged by the SVM within the procedure.} + ] + +Procedures are ranked by a @bold{score}, which summarizes the other data in the table. +Procedures with higher scores are more likely to be bottlenecks, +and should be investigated first for performance issues. + + +@subsection[#:tag "sec:sympro:opts"]{Options and Caveats} + +By default, the symbolic profiler instruments only code that is within +a module whose initial module path is either @tt{rosette} or @tt{rosette/safe}. +In other words, only files beginning with @tt{#lang rosette} or @tt{#lang rosette/safe} will be instrumented. +To instrument @emph{all} code, use the @DFlag{racket} flag described below. + +The @exec{raco symprofile @nonterm{prog}} command accepts the following command-line flags: + +@itemlist[ + @item{@DFlag{stream} --- stream profile data to a browser while executing + the program, rather than producing output only once the program completes. + This option is useful for programs that do not terminate, or take a very long + time to run.} + + @item{@Flag{d} @nonterm{delay} --- delay between samples when using the @DFlag{stream} option, + in seconds (defaults to 2 s).} + + @item{@Flag{m} @nonterm{module-name} --- run the specified @nonterm{module-name} + submodule of @nonterm{prog} (defaults to the @tt{main} submodule).} + + @item{@Flag{t} @nonterm{threshold} --- prune function calls whose execution time is less + than @nonterm{threshold} milliseconds (defaults to 1 ms).} + + @item{@DFlag{racket} --- instrument code in any module, not just those + derived from Rosette.} + + ] + + + + +@section{Walkthrough: Debugging Rosette Performance} + +To illustrate a typical Rosette performance debugging process, +consider building a small solver-aided program +for verifying optimizations in a toy calculator language. +First, we define the calculator language, +in which programs are lists of operations, +and specify its semantics +with a simple recursive interpreter: + +@interaction[#:eval rosette-eval +(code:comment "Calculator opcodes.") +(define-values (Add Sub Sqr Nop) + (values (bv 0 2) (bv 1 2) (bv 2 2) (bv 3 2))) + +(code:comment "An interpreter for calculator programs.") +(code:comment "A program is a list of '(op) or '(op arg) instructions") +(code:comment "that update acc, where op is a 2-bit opcode and arg is") +(code:comment "a 4-bit constant.") +(define (calculate prog [acc (bv 0 4)]) + (cond ; An interpreter for + [(null? prog) acc] ; calculator programs. + [else ; A program is list of + (define ins (car prog)) ; '(op) or '(op arg) + (define op (car ins)) ; instructions that up- + (calculate ; date acc, where op is + (cdr prog) ; a 2-bit opcode and arg + (cond ; is a 4-bit constant. + [(eq? op Add) (bvadd acc (cadr ins))] + [(eq? op Sub) (bvsub acc (cadr ins))] + [(eq? op Sqr) (bvmul acc acc)] + [else acc]))]))] + +One potential optimization for programs in this language +is to replace subtractions with additions. +The @tt{sub->add} procedure performs this operation +at a given index in a program: + +@interaction[#:eval rosette-eval +(code:comment "Functionally sets lst[idx] to val.") +(define (list-set lst idx val) ; Functionally sets + (match lst ; lst[idx] to val. + [(cons x xs) + (if (= idx 0) + (cons val xs) + (cons x (list-set xs (- idx 1) val)))] + [_ lst])) + +(code:comment "Replaces Sub with Add if possible.") +(define (sub->add prog idx) ; Replaces Sub with + (define ins (list-ref prog idx)) ; Add if possible. + (if (eq? (car ins) Sub) + (list-set prog idx (list Add (bvneg (cadr ins)))) + prog))] + +To check that this optimization is correct, +we implement a tiny verification tool @tt{verify-xform} +that constructs a symbolic calculator program of size @tt{N}, +applies the optimization, +and checks that the original and optimized programs +produce the same outputs: + +@interaction[#:eval rosette-eval +(code:comment "Verifies the given transform for all programs of length N.") +(define (verify-xform xform N) ; Verifies the given + (define P ; transform for all + (for/list ([i N]) ; programs of length N. + (define-symbolic* op (bitvector 2)) + (define-symbolic* arg (bitvector 4)) + (if (eq? op Sqr) (list op) (list op arg)))) + (define-symbolic* acc (bitvector 4)) + (define-symbolic* idx integer?) + (define xP (xform P idx)) + (verify ; ∀ acc, idx, P. P(acc) = xform(P, idx)(acc) + (assert (eq? (calculate P acc) (calculate xP acc))))) +] + +We can verify @tt{sub->add} for all calculator programs of size 5: + +@interaction[#:eval rosette-eval +(verify-xform sub->add 5) +] + +which produces no counterexamples, as expected. + +@(rosette-eval '(clear-asserts!)) +@(rosette-eval '(clear-terms!)) +@subsection{Performance Bottlenecks} + +Verifying @tt{sub->add} for larger values of @tt{N} +causes the performance of @tt{verify-xform} to degrade, +from less than a second when @tt{N} = 5 to +a dozen seconds when @tt{N} = 20. +To identify the source of this performance issue, +we can invoke the @tech{symbolic profiler} on the verifier, +producing the output below (after selecting the "Collapse solver time" checkbox): + +@(image profile-xform.png #:scale 0.7) + +The symbolic profiler identifies @tt{list-set} as the bottleneck in this program. +The output shows that @tt{list-set} creates many symbolic terms, +and performs many symbolic operations (the "Union Size" and "Merge Count" columns). + +The core issue here is an @tech{algorithmic mismatch}: +@tt{list-set} makes a recursive call guarded by a short-circuiting condition +@racket[(= idx 0)] that is symbolic when @racket[idx] is unknown. +When a condition's truth value is unknown, +the Rosette SVM must execute both branches of the conditional, +and then merge the two resulting values together +under @tech{path conditions} that summarize the branching decisions +required to reach each value. +In this example, this @emph{symbolic execution} +therefore always executes the recursive call, +and each call creates a larger path condition, +since it must summarize all the previous recursive calls. +This behavior leads to a quadratic growth in the symbolic representation of the list +returned by @tt{list-set}: +@interaction[#:eval rosette-eval +(define-symbolic* idx integer?) +(list-set '(1 2 3) idx 4)] + +The solution is to alter @tt{list-set} to recurse unconditionally: + +@interaction[#:eval rosette-eval +(define (list-set* lst idx val) + (match lst + [(cons x xs) + (cons (if (= idx 0) val x) + (list-set* xs (- idx 1) val))] + [_ lst]))] + +In this revision, the SVM still evaluates both branches +of the conditional, but neither side of the conditional recurses, +and so the path conditions no longer grow quadratically. + +@interaction[#:eval rosette-eval +(list-set* '(1 2 3) idx 4)] + +The performance of @tt{verify-xform} after this change +improves by 2× for @tt{N} = 20. \ No newline at end of file diff --git a/rosette/doc/guide/scribble/performance/profile-xform.png b/rosette/doc/guide/scribble/performance/profile-xform.png new file mode 100644 index 00000000..179b3e40 Binary files /dev/null and b/rosette/doc/guide/scribble/performance/profile-xform.png differ diff --git a/rosette/doc/guide/scribble/performance/profile.png b/rosette/doc/guide/scribble/performance/profile.png new file mode 100644 index 00000000..0441a844 Binary files /dev/null and b/rosette/doc/guide/scribble/performance/profile.png differ diff --git a/rosette/doc/guide/scribble/refs.scrbl b/rosette/doc/guide/scribble/refs.scrbl index a50c5205..f11a6b09 100644 --- a/rosette/doc/guide/scribble/refs.scrbl +++ b/rosette/doc/guide/scribble/refs.scrbl @@ -21,3 +21,9 @@ #:date 2014 #:location "Programming Language Design and Implementation (PLDI)")) +@(define sympro:oopsla18 + (make-bib + #:title @hyperlink["https://unsat.cs.washington.edu/papers/bornholt-sympro.pdf"]{Finding Code That Explodes Under Symbolic Evaluation} + #:author (authors "James Bornholt" "Emina Torlak") + #:date 2018 + #:location "Object Oriented Programming, Systems, Languages, and Applications (OOPSLA)")) \ No newline at end of file diff --git a/rosette/doc/guide/scribble/rosette-guide.scrbl b/rosette/doc/guide/scribble/rosette-guide.scrbl index 9b7c41d0..2ef4c97d 100644 --- a/rosette/doc/guide/scribble/rosette-guide.scrbl +++ b/rosette/doc/guide/scribble/rosette-guide.scrbl @@ -34,6 +34,7 @@ Chapters @seclink["ch:syntactic-forms"]{3}-@seclink["ch:libraries"]{6} define th @include-section["libs/libraries.scrbl"] @include-section["reflection/symbolic-reflection.scrbl"] @include-section["unsafe/unsafe.scrbl"] +@include-section["performance/performance.scrbl"] @(require (only-in "refs.scrbl" generate-bibliography)) @(define bib @(generate-bibliography #:tag "refs" #:sec-title "References")) diff --git a/rosette/doc/guide/scribble/util/lifted.rkt b/rosette/doc/guide/scribble/util/lifted.rkt index d48b41a2..d0b25d89 100644 --- a/rosette/doc/guide/scribble/util/lifted.rkt +++ b/rosette/doc/guide/scribble/util/lifted.rkt @@ -26,14 +26,14 @@ [(? symbol?) (printf "'~a" v)] [_ (printf "~a" v)])) -(define (rosette-evaluator [eval-limits #f]) +(define (rosette-evaluator [eval-limits #f] [lang 'rosette/safe]) (parameterize ([sandbox-output 'string] [sandbox-error-output 'string] [sandbox-path-permissions `((execute ,(byte-regexp #".*")))] [sandbox-memory-limit #f] [sandbox-eval-limits eval-limits] [current-print rosette-printer]) - (make-evaluator 'rosette/safe))) + (make-evaluator lang))) (define (logfile root [filename "log"]) (build-path root (format "~a.txt" filename))) @@ -59,9 +59,9 @@ (define (serializing-evaluator evaluator) (lambda (expr) (serialize-for-logging (evaluator expr)))) -(define (rosette-log-evaluator logfile [eval-limits #f]) +(define (rosette-log-evaluator logfile [eval-limits #f] [lang 'rosette/safe]) (if (file-exists? logfile) (make-log-based-eval logfile 'replay) - (parameterize ([current-eval (serializing-evaluator (rosette-evaluator eval-limits))]) + (parameterize ([current-eval (serializing-evaluator (rosette-evaluator eval-limits lang))]) (make-log-based-eval logfile 'record)))) diff --git a/rosette/info.rkt b/rosette/info.rkt index 19c25e5b..2cab14f3 100644 --- a/rosette/info.rkt +++ b/rosette/info.rkt @@ -17,3 +17,9 @@ ;; Runs the code in `private/install.rkt` before installing this collection. (define pre-install-collection "private/install.rkt") (define compile-omit-files '("private/install.rkt")) + +(define raco-commands + '(("symprofile" + rosette/lib/profile/raco + "profile Rosette symbolic execution" + #f))) diff --git a/rosette/lib/profile.rkt b/rosette/lib/profile.rkt new file mode 100644 index 00000000..5ce6bb2b --- /dev/null +++ b/rosette/lib/profile.rkt @@ -0,0 +1,15 @@ +#lang racket + +(require "profile/tool.rkt" "profile/compile.rkt") + +; The symbolic profiler can be run in two ways: +; (1) require this module in your code, wrap the code you wish to profile in +; (profile-thunk (thunk ...)), and invoke racket with: +; $ racket -l rosette/lib/profile -t path/to/file.rkt +; (2) invoke +; $ raco symprofile path/to/file.rkt +; to profile the entire execution of a file. + +(current-compile symbolic-profile-compile-handler) + +(provide profile-thunk profile) \ No newline at end of file diff --git a/rosette/lib/profile/compile.rkt b/rosette/lib/profile/compile.rkt new file mode 100644 index 00000000..a40f679f --- /dev/null +++ b/rosette/lib/profile/compile.rkt @@ -0,0 +1,485 @@ +#lang racket + +(require syntax/stx syntax/kerncase racket/keyword-transform + (only-in "record.rkt" record-apply! record-source!)) +(provide symbolic-profile-compile-handler symbolic-profile-rosette-only?) + + +;; This file implements profiler instrumentation based on the `errortrace/stacktrace` library. +;; Our instrumentation makes two changes to fully-expanded modules: +;; 1. It wraps procedure calls (instances of #%app) with a call to record-apply! +;; 2. It wraps definition forms (define-values, let-values, etc) that define lambdas +;; with a call to record-source! +;; We need to reimplement errortrace to get (2), since the errortrace library only lets us +;; annotate expression forms. +;; Most code here is copied from errortrace and simplfied to remove features we don't use. + + +;; If true (the default), we'll only instrument modules whose base language +;; is rosette or rosette/safe. Otherwise we'll instrument every module. +(define symbolic-profile-rosette-only? (make-parameter #t)) + + +;; Misc syntax munging stuff --------------------------------------------------- + +(define base-phase + (variable-reference->module-base-phase (#%variable-reference))) +(define code-insp (variable-reference->module-declaration-inspector + (#%variable-reference))) +(define (rearm orig new) + (syntax-rearm new orig)) +(define (disarm stx) + (syntax-disarm stx code-insp)) +(define (location stx) + (let ([src (syntax-source stx)]) + (define srcfile (path->string* src)) + (list srcfile (syntax-line stx) (syntax-column stx)))) +(define (id=? id name) + (and (identifier? id) (equal? (syntax-e id) name))) +(define (keep-lambda-properties orig new) + (let ([p (syntax-property orig 'method-arity-error)] + [p2 (syntax-property orig 'inferred-name)]) + (let ([new (if p + (syntax-property new 'method-arity-error p) + new)]) + (if p2 + (syntax-property new 'inferred-name p2) + new)))) +(define (rebuild expr replacements) + (let loop ([expr expr] [same-k (lambda () expr)] [diff-k (lambda (x) x)]) + (let ([a (assq expr replacements)]) + (cond + [a (diff-k (cdr a))] + [(pair? expr) + (loop (car expr) + (lambda () + (loop (cdr expr) same-k + (lambda (y) (diff-k (cons (car expr) y))))) + (lambda (x) + (loop (cdr expr) + (lambda () (diff-k (cons x (cdr expr)))) + (lambda (y) (diff-k (cons x y))))))] + [(vector? expr) + (loop (vector->list expr) same-k + (lambda (x) (diff-k (list->vector x))))] + [(box? expr) + (loop (unbox expr) same-k (lambda (x) (diff-k (box x))))] + [(syntax? expr) + (if (identifier? expr) + (same-k) + (loop (syntax-e expr) same-k + (lambda (x) (diff-k (datum->syntax expr x expr expr)))))] + [else (same-k)])))) + + + +;; Path management ------------------------------------------------------------- + +;; Convert a path to a string +(define (path->string* path) + (if (path? path) (path->string path) path)) + +;; We shouldn't instrument procedure calls to Rosette functions +;; that aren't actually exported by Rosette +(define (should-instrument-path? path) + (define the-path (path->string* path)) + (or (not the-path) + (not (or (string-contains? the-path "rosette/base/form/module.rkt") + (string-contains? the-path "rosette/base/form/control.rkt") + (string-contains? the-path "rosette/query/form.rkt") + (string-contains? the-path "rosette/query/core.rkt"))))) + + + +;; Instrumentation for procedure applications ---------------------------------- + +;; This is a giant hack to detect applications of keyword procedures. +;; Macro expansion of the definition and application of a keyword procedure +;; hides the information we need, with no simple way to get it back +;; (the "is original?" syntax property we add below is not preserved). +;; So we have this heuristic to try to detect applications of keyword procedures +;; that originated in a file we're instrumenting. +(define original-files (mutable-set)) +(define (is-keyword-procedure-application? expr head) + ; Here are the rules: + ; - if `head` has an alias or converted args property, it must be a kw procedure application. + ; return #t iff the source of that property is not in rosette/query/form.rkt. + ; - otherwise, it's a kw procedure application if the source of the expr is kw.rkt. + ; - if `head` has a source, it must be an original file + ; - if `head` has no source, assume it's a kw procedure application unless it's the + ; `unpack` procedure generated by kw.rkt + (define source-property + (or (syntax-procedure-alias-property head) + (syntax-procedure-converted-arguments-property head))) + (cond + [source-property + (define src (syntax-source (if (pair? source-property) (car source-property) source-property))) + (should-instrument-path? src)] + [(and (syntax-source expr) + (string-contains? (path->string* (syntax-source expr)) + "racket/private/kw.rkt")) + (if (syntax-source head) + (set-member? original-files (syntax-source head)) + (let ([s (syntax->datum head)]) + (or (not (symbol? s)) + (not (string-prefix? (symbol->string s) "unpack")))))] + [else + #f])) + + +;; Produce a list of syntax objects that quote each item in the input list +(define (quote-list lst phase) + (with-syntax ([qt (syntax-shift-phase-level #'quote (- phase base-phase))]) + (for/list ([x lst]) (quasisyntax (qt #,x))))) + + +;; Wrap an application (passed as the syntax list `rest`, without the #%app first element) +;; with a call to the record-apply! instrumentation +(define (instrument-application stx rest phase) + (with-syntax ([record-apply! record-apply!] + [app (syntax-shift-phase-level #'#%plain-app (- phase base-phase))] + [qt (syntax-shift-phase-level #'quote (- phase base-phase))] + [lst (syntax-shift-phase-level #'list (- phase base-phase))]) + (quasisyntax (app (qt record-apply!) (app lst #,@(quote-list (location stx) phase)) #,@rest)))) + + + +;; Instrumentation for let bindings and define-values -------------------------- + +;; Rewrite a definition of the form +;; (let (names rhs) body) +;; to record source instrumentation when the rhs is a plain-lambda. +(define (instrument-binding phase names rhs [lv #'let-values]) + (let* ([rhs* (annotate rhs phase)] + [rhs** (syntax-case rhs* (#%plain-lambda) + [(#%plain-lambda . rest) + (for/or ([n (syntax->list names)]) (is-original? n)) + (with-syntax ([record-source! record-source!] + [app (syntax-shift-phase-level #'#%plain-app (- phase base-phase))] + [qt (syntax-shift-phase-level #'quote (- phase base-phase))] + [lst (syntax-shift-phase-level #'list (- phase base-phase))] + [lv (syntax-shift-phase-level lv (- phase base-phase))] + [values (syntax-shift-phase-level #'values (- phase base-phase))]) + (quasisyntax + (lv ([#,names #,rhs*]) + (app (qt record-source!) (app lst #,@(syntax->list names)) (app lst #,@(quote-list (location rhs*) phase))) + #,(if (one-name names) ; optimization for single return value + (car (syntax-e names)) + #`(app values #,@(syntax->list names))))))] + [_ rhs*])]) + (cons rhs rhs**))) + + + +;; Core instrumentation procedure ---------------------------------------------- + +;; Recursively annotate a lambda expression +(define (annotate-lambda expr clause bodys-stx phase) + (let* ([bodys (stx->list bodys-stx)] + [bodyl (map (lambda (e) (annotate e phase)) bodys)]) + (rebuild clause (map cons bodys bodyl)))) + +;; Recursively annotate a sequence +(define (annotate-seq expr bodys-stx annotate phase) + (let* ([bodys (syntax->list bodys-stx)] + [bodyl (map (lambda (b) (annotate b phase)) bodys)]) + (rebuild expr (map cons bodys bodyl)))) + +;; Recursively annotate a submodule +(define (annotate-module expr disarmed-expr phase) + (define shifted-disarmed-expr + (syntax-shift-phase-level disarmed-expr (- phase))) + (syntax-case shifted-disarmed-expr () + [(mod name init-import mb) + (syntax-case (disarm #'mb) () + [(__plain-module-begin body ...) + ;; Just wrap body expressions + (let ([bodys (syntax->list (syntax (body ...)))]) + (let ([bodyl (map (lambda (b) (annotate-top b 0)) bodys)] + [mb #'mb]) + (rearm + expr + (syntax-shift-phase-level + (rebuild + shifted-disarmed-expr + (list (cons + mb + (rearm + mb + (rebuild mb (map cons bodys bodyl)))))) + phase))))])])) + +;; Return the first element in a list of syntax objects if the list has length 1 +(define (one-name names-stx) + (let ([l (syntax->list names-stx)]) + (and (pair? l) + (null? (cdr l)) + (car l)))) + + +;; Create a top-level annotation procedure that recurses down the syntax of a +;; fully expanded module. +;; This form is copied from errortrace, with the features we don't use removed, +;; and with additional annotation for let bindings and top level define-values. +(define (make-annotate top?) + (lambda (expr phase) + (define disarmed-expr (disarm expr)) + (kernel-syntax-case/phase disarmed-expr phase + [_ + (identifier? expr) + expr] + [(#%top . id) + expr] + [(#%variable-reference . _) + expr] + [(define-values names rhs) + top? + (let* ([rhsl (instrument-binding phase #'names #'rhs #'letrec-values)]) + (rebuild disarmed-expr (list rhsl)))] + + [(begin . exprs) + top? + (rearm + expr + (annotate-seq disarmed-expr + (syntax exprs) + annotate-top phase))] + [(define-syntaxes (name ...) rhs) + top? + (let* ([marked (annotate (syntax rhs) (add1 phase))] + ;; cover at THIS phase, since thats where its bound + [rebuilt (rebuild disarmed-expr (list (cons #'rhs marked)))]) + (rearm expr rebuilt))] + [(begin-for-syntax . exprs) + top? + (rearm + expr + (annotate-seq disarmed-expr + (syntax exprs) + annotate-top + (add1 phase)))] + + [(module name init-import mb) + (annotate-module expr disarmed-expr 0)] + [(module* name init-import mb) + (annotate-module expr disarmed-expr (if (syntax-e #'init-import) 0 phase))] + + [(#%expression e) + (rearm expr #`(#%expression #,(annotate (syntax e) phase)))] + + ;; No way to wrap + [(#%require i ...) expr] + ;; No error possible (and no way to wrap) + [(#%provide i ...) expr] + [(#%declare i ...) expr] + + ;; No error possible + [(quote _) + expr] + [(quote-syntax . _) + expr] + + ;; Wrap body, also a profile point + [(#%plain-lambda args . body) + (rearm + expr + (keep-lambda-properties + expr + (annotate-lambda expr disarmed-expr (syntax body) + phase)))] + [(case-lambda clause ...) + (with-syntax ([([args . body] ...) + (syntax (clause ...))]) + (let* ([clauses (syntax->list (syntax (clause ...)))] + [clausel (map + (lambda (body clause) + (annotate-lambda + expr clause body phase)) + (syntax->list (syntax (body ...))) + clauses)]) + (rearm + expr + (keep-lambda-properties + expr + (rebuild disarmed-expr (map cons clauses clausel))))))] + + ;; Wrap RHSs and body + [(let-values ([vars rhs] ...) . body) + (let ([rhsl (map (curryr (curry instrument-binding phase) #'let-values) + (syntax->list #'(vars ...)) + (syntax->list #'(rhs ...)))] + [bodysl (map (lambda (body) (cons body (annotate body phase))) (syntax->list #'body))]) + (rearm + expr + (rebuild disarmed-expr (append rhsl bodysl))))] + [(letrec-values ([vars rhs] ...) . body) + (let ([rhsl (map (curryr (curry instrument-binding phase) #'letrec-values) + (syntax->list #'(vars ...)) + (syntax->list #'(rhs ...)))] + [bodysl (map (lambda (body) (cons body (annotate body phase))) (syntax->list #'body))]) + (rearm + expr + (rebuild disarmed-expr (append rhsl bodysl))))] + ;; This case is needed for `#lang errortrace ...', which uses + ;; `local-expand' on the module body. + [(letrec-syntaxes+values sbindings ([vars rhs] ...) . body) + (let ([rhsl (map (curryr (curry instrument-binding phase) #'letrec-values) + (syntax->list #'(vars ...)) + (syntax->list #'(rhs ...)))] + [bodysl (map (lambda (body) (cons body (annotate body phase))) (syntax->list #'body))]) + (rearm + expr + (rebuild disarmed-expr (append rhsl bodysl))))] + + ;; Wrap RHS + [(set! var rhs) + (let ([new-rhs (annotate (syntax rhs) phase)]) + (rearm + expr + (rebuild disarmed-expr (list (cons #'rhs new-rhs)))))] + + ;; Wrap subexpressions only + [(begin e) + ;; Single expression: no mark + (rearm + expr + #`(begin #,(annotate (syntax e) phase)))] + [(begin . body) + (rearm + expr + (annotate-seq disarmed-expr #'body annotate phase))] + [(begin0 . body) + (rearm + expr + (annotate-seq disarmed-expr #'body annotate phase))] + [(if tst thn els) + (let ([w-tst (annotate (syntax tst) phase)] + [w-thn (annotate (syntax thn) phase)] + [w-els (annotate (syntax els) phase)]) + (rearm + expr + (rebuild disarmed-expr (list (cons #'tst w-tst) + (cons #'thn w-thn) + (cons #'els w-els)))))] + [(with-continuation-mark . body) + (rearm + expr + (annotate-seq disarmed-expr (syntax body) + annotate phase))] + + ;; Wrap whole application, plus subexpressions + [(#%plain-app . rest) + (if (stx-null? (syntax rest)) + expr + (let* ([head (car (syntax-e #'rest))] + [restl (map (lambda (body) (cons body (annotate body phase))) (syntax->list #'rest))]) + (rearm + expr + (rebuild + (if (or (and (or (is-original? expr) + (is-original? head)) + (should-instrument-path? (syntax-source head))) + (is-keyword-procedure-application? expr head)) + (instrument-application expr #'rest phase) + disarmed-expr) + restl))))] + [_else + (error 'errortrace "unrecognized expression form~a~a: ~.s" + (if top? " at top-level" "") + (if (zero? phase) "" (format " at phase ~a" phase)) + (syntax->datum expr))]))) + +;; Create two annotation procedures: one for top-level forms and one for everything else +(define annotate (make-annotate #f)) +(define annotate-top (make-annotate #t)) + + + +;; Original syntax detection --------------------------------------------------- +;; This hack is copied from errortrace to detect "original" parts of the input +;; program. + +;; Mark original syntax +(define annotate-key 'sympro:original) +(define (add-annotate-property s) + (cond + [(syntax? s) + (define new-s (syntax-rearm + (let ([s (disarm s)]) + (datum->syntax s + (add-annotate-property (syntax-e s)) + s + s)) + s)) + (syntax-property new-s annotate-key #t #t)] + [(pair? s) + (cons (add-annotate-property (car s)) + (add-annotate-property (cdr s)))] + [(vector? s) + (for/vector #:length (vector-length s) ([e (in-vector s)]) + (add-annotate-property e))] + [(box? s) (box (add-annotate-property (unbox s)))] + [(prefab-struct-key s) + => (lambda (k) + (apply make-prefab-struct + k + (add-annotate-property (cdr (vector->list (struct->vector s))))))] + [(and (hash? s) (immutable? s)) + (cond + [(hash-eq? s) + (for/hasheq ([(k v) (in-hash s)]) + (values k (add-annotate-property v)))] + [(hash-eqv? s) + (for/hasheqv ([(k v) (in-hash s)]) + (values k (add-annotate-property v)))] + [else + (for/hash ([(k v) (in-hash s)]) + (values k (add-annotate-property v)))])] + [else s])) + +;; Is this syntax original? +(define (is-original? stx) + (and (syntax-source stx) + (syntax-property stx annotate-key))) + + + +;; Compile handler ------------------------------------------------------------- + +;; Annotate a top-level expr +(define (profile-annotate stx) + (define annotate-id + (syntax-case stx () + [(mod id lang (mod-begin forms ...)) + (and (id=? #'mod 'module) + (or (id=? #'lang 'rosette) + (id=? #'lang 'rosette/safe) + (not (symbolic-profile-rosette-only?))) + (id=? #'mod-begin '#%module-begin)) + #'id] + [_ #f])) + (if annotate-id + (let () + (printf "INSTRUMENTING ~v\n" (syntax-source stx)) + (set-add! original-files (syntax-source stx)) + (define expanded-e (expand-syntax (add-annotate-property stx))) + (annotate-top expanded-e (namespace-base-phase))) + stx)) + + +;; Create a compile handler that invokes profile-annotate on +;; a piece of syntax that needs compilation, and then runs the +;; existing (current-compile) +(define (make-symbolic-profile-compile-handler) + (define orig (current-compile)) + (lambda (e immediate-eval?) + (orig (profile-annotate + (if (syntax? e) + e + (namespace-syntax-introduce + (datum->syntax #f e)))) + immediate-eval?))) + + +(define symbolic-profile-compile-handler + (make-symbolic-profile-compile-handler)) diff --git a/rosette/lib/profile/data.rkt b/rosette/lib/profile/data.rkt new file mode 100644 index 00000000..9d8cfca2 --- /dev/null +++ b/rosette/lib/profile/data.rkt @@ -0,0 +1,42 @@ +#lang racket + +(provide (all-defined-out)) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Profiler data structures + +;; A profile consists of a mutable box events, which contains a list of +;; profile-event? records. +(struct profile-state (events thd)) + +(struct profile-event (metrics)) +(struct profile-event-enter profile-event (id location procedure inputs) #:transparent) +(struct profile-event-exit profile-event (outputs)) +(struct profile-event-sample profile-event ()) +(struct profile-event-pc profile-event ()) +(struct profile-event-solve profile-event ()) +(struct profile-event-solve-start profile-event-solve ()) +(struct profile-event-solve-finish profile-event-solve (sat?)) +(struct profile-event-solve-encode profile-event-solve (asserts)) +(struct profile-event-finitize profile-event ()) +(struct profile-event-finitize-start profile-event-finitize ()) +(struct profile-event-finitize-finish profile-event-finitize ()) +(struct profile-event-encode profile-event ()) +(struct profile-event-encode-start profile-event-encode ()) +(struct profile-event-encode-finish profile-event-encode ()) +(struct profile-event-term profile-event (val)) +(struct profile-event-term-new profile-event-term ()) + +;; Returns a new profile +(define (make-profile-state) + (profile-state (box '()) (current-thread))) + +;; Append a new event to a profile state +(define (profile-state-append! state evt) + (define the-box (profile-state-events state)) + (let loop () + (let* ([old (unbox the-box)] + [new (cons evt old)]) + (unless (box-cas! the-box old new) + (loop))))) diff --git a/rosette/lib/profile/feature.rkt b/rosette/lib/profile/feature.rkt new file mode 100644 index 00000000..b7428ffa --- /dev/null +++ b/rosette/lib/profile/feature.rkt @@ -0,0 +1,119 @@ +#lang racket + +(require (only-in rosette union? union-contents union expression) + (only-in rosette/base/core/type get-type typed? type-deconstruct) + (only-in rosette/base/core/term term?)) + +(provide (except-out (all-defined-out) flatten-value)) + + + +; Stores a procedure that takes as input a list of values +; and outputs a number that characterizes the cost of those values. +(struct feature (name procedure) + #:property prop:procedure + [struct-field-index procedure] + #:guard (lambda (name proc id) + (unless (procedure? proc) + (error 'feature "Expected a procedure?, given ~a" proc)) + (values name proc)) + #:methods gen:custom-write + [(define (write-proc self port mode) + (fprintf port "(feature ~a)" (feature-name self)))]) + + +; A simple feature that returns the sum of the sizes of the input unions. +(define union-size-feature + (feature + 'union-size + (lambda (xs) + (for/sum ([x xs] #:when (union? x)) + (length (union-contents x)))))) + + +; Updates the footprint map to contain the object graph of x. +; The footprint is a set of key-value pairs, where the key is an +; object (a node in the graph), and the value is the number of +; outgoing edges. Symbolic terms, regardless of size, are treated +; as opaque values with no outgoing edges (just like concrete constants). +(define (measure! footprint x) + (unless (hash-has-key? footprint x) + (match x + [(union gvs) + (hash-set! footprint x (length gvs)) + (for ([gv gvs]) ; don't count the guards + (measure! footprint (cdr gv)))] + [(? list? children) + (hash-set! footprint x (length children)) + (for ([c children]) + (measure! footprint c))] + [(cons a b) + (hash-set! footprint x 2) + (measure! footprint a) + (measure! footprint b)] + [(? vector?) + (hash-set! footprint x (vector-length x)) + (for ([c x]) + (measure! footprint c))] + [(box c) + (hash-set! footprint x 1) + (measure! footprint c)] + [(? typed?) + (match (type-deconstruct (get-type x) x) + [(list (== x)) (hash-set! footprint x 0)] + [children + (hash-set! footprint x (length children)) + (for ([c children]) + (measure! footprint c))])] + [_ (hash-set! footprint x 0)]))) + + + +; A simple feature that measures V + E, where V is the number of vertices and +; E is the number of edges that make up the input object graph. +(define heap-size-feature + (feature + 'heap-size + (let ([cache (make-hash)]) + (lambda (xs) + (hash-ref! cache xs + (thunk + (define footprint (make-hash)) + (for ([x xs]) (measure! footprint x)) + (+ (hash-count footprint) + (for/sum ([v (in-hash-values footprint)]) v)))))))) + + +; A feature that determines the "signature" of a function call -- the types of +; each of its inputs (union, symbolic, or concrete). +(define signature-feature + (feature + 'signature + (lambda (xs) + (let loop ([xs xs]) + (cond + [(null? xs) '()] + [else (let ([x (car xs)]) + (cons (cond + [(union? x) 'union] + [(term? x) 'symbolic] + [else 'concrete]) + (loop (cdr xs))))]))))) + + +; A parameter that holds a list of features to profile. +(define enabled-features + (list signature-feature)) + + +; Convert a feature hash into a plain hash with symbols for keys and s-exps for values +(define (flatten-value x) + (cond + [(list? x) (map flatten-value x)] + [(hash? x) (for/hash ([(k v) x]) (values k (flatten-value v)))] + [(symbol? x) (symbol->string x)] + [else x])) +(define (features->flat-hash feats) + (for/hash ([f/v feats]) + (match-define (cons k v) f/v) + (values (feature-name k) (flatten-value v)))) diff --git a/rosette/lib/profile/graph.rkt b/rosette/lib/profile/graph.rkt new file mode 100644 index 00000000..bd31ffdf --- /dev/null +++ b/rosette/lib/profile/graph.rkt @@ -0,0 +1,84 @@ +#lang racket + +(require racket/hash racket/struct + "data.rkt" "record.rkt" "reporter.rkt" "feature.rkt") +(provide (all-defined-out)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Profile graph data structures + +;; A profile node is an entry in the dynamic control flow graph of the +;; profiled code. It contains a pointer to its parent node, +;; a list of children nodes, and a profile-data struct that contains the +;; actual data for the profile. +(struct profile-node (id parent children data) #:mutable + #:methods gen:custom-write + [(define write-proc + (make-constructor-style-printer + (lambda (obj) 'profile-node) + (lambda (obj) (let ([d (profile-node-data obj)]) + (if d + (list (let ([proc (profile-data-procedure d)]) + (if (symbol? proc) proc (object-name proc))) + (metrics-ref (profile-data-start d) 'time)) + (list #f #f))))))]) + + +;; Profile data for a single procedure invocation. +;; * The location field stores the location at which the given procedure was +;; invoked. +;; * The procedure field is the invoked procedure +;; * The inputs and outputs fields are assoc lists from features to numbers. +;; For each feature in enabled-features, they store the value of that +;; feature for the inputs and outputs of the current invocation. +;; * The metrics field is a hash map from symbols to numbers, where each +;; symbol describes a performance metric collected during symbolic evaluation, +;; e.g., cpu time, real time, gc time, the number of merge invocations, the number +;; of unions and terms created, etc. +;; * The start and finish fields track the value of various metrics at the entry +;; and exit to the current invocation, respectively. +(struct profile-data (location procedure inputs outputs metrics start finish) #:mutable #:transparent) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Conversion + +;; Convert an instance of profile-state (i.e., a list of profile events) into a +;; dynamic call graph representation. +(define (profile-state->graph state) + (define events (reverse (unbox (profile-state-events state)))) + (unless (profile-event-enter? (first events)) + (error 'profile-state->graph "expected first event to be profile-event-enter")) + (define node #f) + (define root #f) + (for ([e events]) + (match e + [(profile-event-enter met id loc proc in) + (define new-in (features->flat-hash in)) + (define new-name proc) + (define new-data (profile-data loc new-name new-in (hash) '() met (hash))) + (define new-node (profile-node id node '() new-data)) + (if (false? node) + (let () + (set-profile-node-parent! new-node new-node) + (set! root new-node)) + (set-profile-node-children! node (append (profile-node-children node) (list new-node)))) + (set! node new-node)] + [(profile-event-exit met out) + (define data (profile-node-data node)) + (define metrics (diff-metrics (profile-data-start data) met)) + (set-profile-data-metrics! data metrics) + (define new-out (features->flat-hash out)) + (set-profile-data-outputs! data new-out) + (set-profile-data-finish! data met) + (set! node (profile-node-parent node))] + [(profile-event-sample met) ; fill in missing statistics up the callgraph + (let rec ([node node]) + (define data (profile-node-data node)) + (define metrics (diff-metrics (profile-data-start data) met)) + (set-profile-data-metrics! data metrics) + (set-profile-data-finish! data met) + (unless (eq? node (profile-node-parent node)) + (rec (profile-node-parent node))))] + [_ void])) + root) diff --git a/rosette/lib/profile/raco.rkt b/rosette/lib/profile/raco.rkt new file mode 100644 index 00000000..7d54d040 --- /dev/null +++ b/rosette/lib/profile/raco.rkt @@ -0,0 +1,103 @@ +#lang racket/base + +(require racket/cmdline + raco/command-name + "compile.rkt" + "tool.rkt" + (only-in "record.rkt" filtering-threshold) + "renderer/trace.rkt" + "renderer/noop.rkt" + "renderer/heap.rkt" + "renderer/report.rkt") + +;; raco symprofile (based on raco feature-profile) +;; profile the main submodule (if there is one), or the top-level module + +(define renderer% (make-parameter make-report-renderer)) +(define run-profiler? (make-parameter #t)) +(define module-name (make-parameter 'main)) +(define renderer-options (make-parameter (hash))) +(define file + (command-line #:program (short-program+command-name) + #:help-labels "" "Profiler modes" + #:once-any ; Profiler selections + ["--trace" "Produce a complete execution trace" + (renderer% make-trace-renderer)] + ["--report" "Produce an interactive report" + (renderer% make-report-renderer)] + ["--stream" "Produce a streaming interactive report" + (renderer% make-report-stream-renderer)] + ["--noop" "Produce no profile output (for testing)" + (renderer% make-noop-renderer)] + ["--heap" "Profile a heap profile" + (renderer% make-heap-renderer)] + ; Tool configuration + #:help-labels "" "Profiled code settings" + #:once-each + [("-l" "--compiler-only") + "Only install the compile handler; do not run the profiler" + (run-profiler? #f)] + [("-m" "--module") name + "Run submodule (defaults to 'main)" + (module-name (string->symbol name))] + [("-r" "--racket") + "Instrument code in any module, not just `#lang rosette`" + (symbolic-profile-rosette-only? #f)] + #:help-labels "" "Profiling settings" + #:once-each + [("-t" "--threshold") t + "Threshold (in milliseconds) for pruning cheap function calls" + (let ([th (string->number t)]) + (when (or (eq? th #f) (< th 0)) + (raise-argument-error 'threshold "number >= 0" t)) + (filtering-threshold th))] + ; Renderer-specific configuration + #:help-labels "" "Mode-specific settings" + #:once-each + [("-d" "--delay") d + "Streaming report: delay between samples, in seconds" + (let ([de (string->number d)]) + (when (or (eq? de #f) (<= de 0)) + (raise-argument-error 'delay "number > 0" d)) + (renderer-options (hash-set (renderer-options) 'interval de)))] + [("-s" "--symlink-html") + "Interactive reports: symlink template instead of copying" + (renderer-options (hash-set (renderer-options) 'symlink #t))] + #:help-labels "" + #:args (filename . args) + ; pass all unused arguments to the file being run + (current-command-line-arguments (list->vector args)) + filename)) + +; Set up the renderer +(define (renderer source-stx name) + ((renderer%) source-stx name (renderer-options))) +(current-renderer renderer) + +(collect-garbage) +(collect-garbage) +(collect-garbage) + +(current-compile symbolic-profile-compile-handler) + + +; check if there's a module of the given name, and if not, +; import the entire file instead +(define (module-to-profile file mod) + (define file-path `(file ,file)) + (define mod-path `(submod ,file-path ,mod)) + (if (module-declared? mod-path #t) + (values mod-path mod-path) + (values file-path file))) + +(define-values (mod mod-pretty) + (module-to-profile file (module-name))) + +(define (run) + (dynamic-require mod #f)) + + +(if (run-profiler?) + (profile-thunk run #:source mod-pretty + #:name (format "~a" file)) + (run)) diff --git a/rosette/lib/profile/record.rkt b/rosette/lib/profile/record.rkt new file mode 100644 index 00000000..07d4e7ce --- /dev/null +++ b/rosette/lib/profile/record.rkt @@ -0,0 +1,155 @@ +#lang racket + +(require rosette/base/core/reporter racket/hash + "data.rkt" "feature.rkt" "reporter.rkt") +(provide (all-defined-out)) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Source location tracking + +;; Represents the global map from procedure objects to their source locations (if known). +(define current-sources (make-parameter (make-hash))) + +;; Records the mapping from the given procedure object to its source info. +(define (record-source! procs location) + (for ([p procs]) + (hash-set! (current-sources) p location))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Profiler run-time state + +;; A parameter that holds the current profile / call stack. +(define current-profile (make-parameter #f)) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Run-time profile filtering + +; Minimum time in milliseconds for a call to be included in the profile. +; Filtering is disabled if this is zero. +(define filtering-threshold (make-parameter 1.0)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Run-time profile node updates + +;; Records a procedure entry by appending a new profile node to the current one. +;; This procedure should be called after all arguments to the profiled procedure +;; have been evaluated, but before the procedure is invoked. +(define-syntax-rule (record-enter profile reporter) + (let ([next-id (let ([n 0]) (thunk (begin0 n (set! n (add1 n)))))]) + (lambda (loc proc in) + (when (and profile (eq? (current-thread) (profile-state-thd profile))) + (let* ([inputs (compute-features in)] + [metrics (get-current-metrics/call reporter)] + [new (profile-event-enter metrics (next-id) loc proc inputs)]) + (profile-state-append! profile new)))))) + +; Default version just uses the current-profile/current-reporter params +(define default-record-enter + (record-enter (current-profile) (current-reporter))) + +(define record-enter! default-record-enter) + + +;; Records a procedure exit by modifying the data for the current profile node. +;; This procedure should be called after the profiled procedure call returns. +(define-syntax-rule (record-exit profile reporter threshold) + (let ([do-record-exit! + (lambda (out) + (let* ([outputs (compute-features out)] + [metrics (get-current-metrics/call reporter)] + [new (profile-event-exit metrics outputs)]) + (profile-state-append! profile new)))]) + (lambda (out) + (when (and profile (eq? (current-thread) (profile-state-thd profile))) + (let* ([curr profile] + [evts (unbox (profile-state-events curr))]) + (cond + [(or (null? evts) + (not (profile-event-enter? (car evts))) + (>= (- (current-inexact-milliseconds) (get-call-time (car evts))) threshold)) + (do-record-exit! out)] + [else ; ==> (and (< real (filter-threshold)) (profile-event-enter? (car evts))) + ; prune the previous event, which was an enter + (define the-box (profile-state-events curr)) + ; if this CAS fails, it can only be because the streaming thread removed + ; all events from the box. if that happened then the ENTER we're trying + ; to delete has already been published, and so we need to retain the + ; corresponding EXIT. + (unless (box-cas! the-box evts (cdr evts)) + (do-record-exit! out))])))))) + +; Default version just uses the current-profile/current-reporter params +(define default-record-exit + (record-exit (current-profile) (current-reporter) (filtering-threshold))) + +(define record-exit! default-record-exit) + + +; Specialize record-enter! and record-exit! +(define (specialize-recorders! profile reporter threshold) + (set! record-enter! (record-enter profile reporter)) + (set! record-exit! (record-exit profile reporter threshold))) +; Undo specialization +(define (reset-specialized-recorders!) + (set! record-enter! default-record-enter) + (set! record-exit! default-record-exit)) + + +;; Record a sample (used when a profile is unfinished) +(define (record-sample!) + (profile-state-append! (current-profile) (get-sample-event))) + + +;; Compute the interesting features of a list of inputs or outputs +(define (compute-features xs) + (list (cons signature-feature (signature-feature xs)))) + + +;; Records the application of a procedure proc at a location loc to +;; the given by-position and/or keyword arguments. +;; The syntax rule generates the instrumentation for the two different +;; cases (keyword arguments or not), which helps expose some fusion +;; optimization opportunities to the compiler. +(define-syntax-rule (runner app-proc-to-in-expr) + (let* ([handler (lambda (exn) (values exn null))] + [returner (lambda e (values #f e))]) + (lambda (loc proc in) + (record-enter! loc proc in) + (call-with-exception-handler + (lambda (e) + (record-exit! '()) + e) + (thunk + (define ret (call-with-values (lambda () app-proc-to-in-expr) list)) + (record-exit! ret) + (apply values ret)))))) +(define record-apply! + (make-keyword-procedure + (lambda (kws kw-args loc proc . rest) + ((runner (keyword-apply proc kws kw-args rest)) loc proc (append rest kw-args))) + (lambda (loc proc . rest) + ((runner (apply proc rest)) loc proc rest)))) + + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Top-level profiler invocation + +;; Run a thunk in a given profiler+reporter context. and return its output +(define (run-profile-thunk proc profile reporter) + (parameterize ([current-profile profile] + [current-reporter reporter]) + (specialize-recorders! profile reporter (filtering-threshold)) + (record-enter! #f 'the-profiled-thunk '()) + (define out + (with-handlers ([exn:break? (lambda (e) + ((error-display-handler) (if (exn? e) (exn-message e) (~e e)) e) + (record-sample!) + '())]) + (define out (call-with-values proc list)) + (record-exit! out) + out)) + (reset-specialized-recorders!) + out)) diff --git a/rosette/lib/profile/renderer/heap.rkt b/rosette/lib/profile/renderer/heap.rkt new file mode 100644 index 00000000..4cdaa11f --- /dev/null +++ b/rosette/lib/profile/renderer/heap.rkt @@ -0,0 +1,205 @@ +#lang racket + +(require net/sendurl + "renderer.rkt" + "../data.rkt" "../reporter.rkt" + (only-in rosette/base/core/term expression)) +(provide make-heap-renderer) + +; The summary renderer aggregates inclusive and exclusive time +; per procedure and prints the results. + +(define (make-heap-renderer source name [options (hash)]) + (heap-renderer source name)) + +(struct heap-renderer (source name) + #:transparent + #:methods gen:renderer + [(define start-renderer void) + (define (finish-renderer self profile) + (match-define (heap-renderer source name) self) + (time (analyze-terms profile)))]) + + +(define (analyze-terms profile) + ; a node in the term graph contains three fields + ; * the term + ; * the subterms (term-node?s) the term contains + ; * the creator of the term (a call-node?) + (struct term-node (term subterms creator)) + + ; a node in the call tree contains five fields + ; * the name of the procedure invoked + ; * the children invoked by the procedure (a list of call-node?s) + ; * the total time spent in this node (including children) + ; * a list of terms created by this call (term-node?s) + (struct call-node (proc parent children time created-terms) #:mutable) + + ; term? -> term-node? + (define term->node (make-hasheq)) + ; term?s that reached the solver + (define solved-terms (mutable-seteq)) + ; current call-node? + (define current-call #f) + + + (define events (reverse (unbox (profile-state-events profile)))) + (for ([e events]) + (match e + [(profile-event-enter met id loc proc in) + ; temporarily store the metrics in the time field -- they'll be removed + ; when we reach the corresponding profile-event-exit? for this call + (define node (call-node (object-name proc) current-call '() met '())) + (when current-call + (set-call-node-children! current-call (cons node (call-node-children current-call)))) + (set! current-call node)] + [(profile-event-exit met out) + ; compute the delta metrics (the initial metrics were stored in the time field) + (define metrics (diff-metrics (call-node-time current-call) met)) + (define time (metrics-ref metrics 'time)) + (set-call-node-time! current-call time) + ; reverse the order of the calls since we built that list backwards + (set-call-node-children! current-call (reverse (call-node-children current-call))) + (unless (false? (call-node-parent current-call)) + (set! current-call (call-node-parent current-call)))] + [(profile-event-term-new met t) + (define children + (match t + [(expression op elts ...) + (for/list ([e elts] #:when (hash-has-key? term->node e)) + (hash-ref term->node e))] + [_ '()])) + (define node (term-node t children current-call)) + (set-call-node-created-terms! current-call (cons node (call-node-created-terms current-call))) + (hash-set! term->node t node)] + [(profile-event-solve-encode met lst) + (for* ([assts lst][a assts]) + (set-add! solved-terms a))] + [_ void])) + + + ; output dot + (define p (make-temporary-file "heap~a.dot")) + (define f (open-output-file p #:exists 'truncate)) + (fprintf f "digraph heap {\n ordering=out;\n") + + + ; decide which nodes will we put in the plot + ; by chosing a time threshold based on max time spent in any one call + (define (call-node-time-excl node) ; exclusive time for a call-node + (- (call-node-time node) (apply + (map call-node-time (call-node-children node))))) + (define maxtime-excl ; max exclusive time over all calls + (let loop ([node current-call]) + (apply max (append (list (call-node-time-excl node)) (map loop (call-node-children node)))))) + (define maxtime-incl ; max inclusive time over all calls + (apply max (map call-node-time (call-node-children current-call)))) + (define threshold (* 0.001 maxtime-incl)) + + ; now select the nodes to include and assign them dot names + (define node->label (make-hasheq)) + (let loop ([node current-call]) + (when (> (call-node-time node) threshold) + (hash-set! node->label node (format "n~v" (hash-count node->label))) + (for ([c (call-node-children node)]) (loop c)))) + + + ; determine which terms (transitively) reached the solver + (define reached-solver (mutable-seteq)) + (for ([t solved-terms]) + (define tn (hash-ref term->node t)) + (let loop ([tn tn]) + (unless (set-member? reached-solver tn) + (set-add! reached-solver tn) + (for ([tn* (term-node-subterms tn)]) (loop tn*))))) + + ; determine the sources of terms that did not reach the solver + (define unused-term-sources (make-hasheq)) + (for ([(t tn) term->node] #:unless (set-member? reached-solver tn)) + (define src (term-node-creator tn)) + (hash-set! unused-term-sources src (add1 (hash-ref unused-term-sources src 0)))) + + + ; emit the definitions for each node + (define (width dt) (exact->inexact (+ .75 (min (/ dt maxtime-excl) 1)))) + (define (height dt) (exact->inexact (+ .5 (min (/ dt maxtime-excl) 1)))) + (define (fontsize dt) (exact->inexact (+ 10 (* 8 (min (/ dt maxtime-excl) 1))))) + (define (color node) + (define score + (let ([num-terms (length (call-node-created-terms node))]) + (if (or (= num-terms 0) (< num-terms (* (hash-count term->node) 0.01))) + 0 + (/ (hash-ref unused-term-sources node 0) num-terms)))) + (define 255-color (inexact->exact (round (* (- 1 score) 255)))) + (define hex-color (~r 255-color #:base 16 #:min-width 2 #:pad-string "0")) + (format "#ff~a~a" hex-color hex-color)) + (let loop ([node current-call]) ; loop in call-graph order + (when (hash-has-key? node->label node) + (let* ([dt (call-node-time node)] + [dt-excl (call-node-time-excl node)] + [dtstr (~r dt #:precision 0)]) + (define label + (format "~a\n~a ms\n~v/~v terms" + (call-node-proc node) + dtstr + (hash-ref unused-term-sources node 0) + (length (call-node-created-terms node)))) + (fprintf f " ~a [shape=box,style=filled,fillcolor=\"~a\",label=\"~a\",width=~v,height=~v,fontsize=~v,fixedsize=true];\n" + (hash-ref node->label node) + (color node) + label + (width dt-excl) (height dt-excl) (fontsize dt-excl))) + (for ([c (call-node-children node)]) (loop c)))) + + + ; put control flow edges in the plot + (let loop ([node current-call]) + (when (hash-has-key? node->label node) + (for ([c (call-node-children node)]) + (when (hash-has-key? node->label c) + (fprintf f " ~a -> ~a;\n" (hash-ref node->label node) (hash-ref node->label c)))) + (for ([c (call-node-children node)] #:when (hash-has-key? node->label c)) (loop c)))) + + + ; compute data flow edges + (define dataflow (make-hasheq)) + (for ([tn (hash-values term->node)]) + (define creator (term-node-creator tn)) + (when (hash-has-key? node->label creator) + (define from (hash-ref! dataflow creator make-hasheq)) + (for ([tn* (term-node-subterms tn)]) + (define creator* (term-node-creator tn*)) + (when (hash-has-key? node->label creator*) + (hash-set! from creator* (add1 (hash-ref from creator* 0))))))) + + ; put data flow edges in the plot + (define flow-counts (flatten (map hash-values (hash-values dataflow)))) + (unless (null? flow-counts) + (define maxdataflow (apply max flow-counts)) + (define dataflow-threshold (* 0.01 maxdataflow)) + (for* ([(src dsts) dataflow] + [(dst v) dsts] + #:when (> v dataflow-threshold)) + (fprintf f " ~a -> ~a [penwidth=~v,color=blue];\n" + (hash-ref node->label src) + (hash-ref node->label dst) + (exact->inexact (* (/ v maxdataflow) 4))))) + + ; close the graph + (fprintf f "}\n") + (close-output-port f) + + + (printf "sources of unused terms: ~v\n" (hash-count unused-term-sources)) + (define sorted-sources + (sort (hash-keys unused-term-sources) > #:key (lambda (cn) (hash-ref unused-term-sources cn)))) + (define worst-sources (take sorted-sources (min 10 (length sorted-sources)))) + (for ([s worst-sources]) + (printf "* ~v - ~v\n" (call-node-proc s) (hash-ref unused-term-sources s))) + + + ; find a place to put the pdf + (define pdf-path (make-temporary-file "graph~a.pdf")) + (system (format "dot -Tpdf -q -o ~a ~a" pdf-path p)) + (send-url/file pdf-path) + + (printf "Output to file: ~a\n" pdf-path)) diff --git a/rosette/lib/profile/renderer/noop.rkt b/rosette/lib/profile/renderer/noop.rkt new file mode 100644 index 00000000..102a62a9 --- /dev/null +++ b/rosette/lib/profile/renderer/noop.rkt @@ -0,0 +1,22 @@ +#lang racket + +(require "../data.rkt" "../record.rkt" "renderer.rkt") +(provide make-noop-renderer) + +; The noop renderer does nothing! + +(define (make-noop-renderer source name [options (hash)]) + (noop-renderer source name)) + +(struct noop-renderer (source name) + #:transparent + #:methods gen:renderer + [(define start-renderer void) + (define (finish-renderer self profile) + (printf "Profiled ~v events.\n" (length (unbox (profile-state-events profile)))) + (define types (make-hash)) + (for ([e (unbox (profile-state-events profile))]) + (define type (vector-ref (struct->vector e) 0)) + (hash-set! types type (add1 (hash-ref types type 0)))) + (for ([type (sort (hash-keys types) > #:key (lambda (t) (hash-ref types t)))]) + (printf "* ~a: ~v\n" type (hash-ref types type))))]) diff --git a/rosette/lib/profile/renderer/renderer.rkt b/rosette/lib/profile/renderer/renderer.rkt new file mode 100644 index 00000000..9d8c595a --- /dev/null +++ b/rosette/lib/profile/renderer/renderer.rkt @@ -0,0 +1,8 @@ +#lang racket + +(require racket/generic) +(provide (all-defined-out)) + +(define-generics renderer + (start-renderer renderer profile reporter) + (finish-renderer renderer profile)) diff --git a/rosette/lib/profile/renderer/report.rkt b/rosette/lib/profile/renderer/report.rkt new file mode 100644 index 00000000..6524eee1 --- /dev/null +++ b/rosette/lib/profile/renderer/report.rkt @@ -0,0 +1,263 @@ +#lang racket + +(require racket/date racket/runtime-path racket/async-channel net/sendurl json + "../data.rkt" "../reporter.rkt" (only-in "../record.rkt" filtering-threshold) + "renderer.rkt" "syntax.rkt" + "report/generic.rkt" "report/ws-server.rkt" + "report/callgraph.rkt" "report/solver.rkt" "report/terms.rkt") +(provide make-report-renderer make-report-stream-renderer) + +; The report renderer produces HTML output by sending +; profile events to a collection of report components, +; which each generate JSON-formatted messages to be passed +; to the client webpage (either by streaming or statically). + + +; The path containing the HTML template +(define-runtime-path template-dir "report/html") + + +; Components that will be generating messages for this profile +(define report-components + (list make-callgraph-component + make-solver-calls-component + make-terms-component)) + + +(struct report-options (symlink? interval) #:transparent) + + +; Make a non-streaming report renderer +(define (make-report-renderer source name [options (hash)]) + (define components (for/list ([c report-components]) (c options))) + (report-renderer source name components (report-options (hash-ref options 'symlink #f) + (hash-ref options 'interval 2.0)))) + +; Make a streaming report renderer +(define (make-report-stream-renderer source name [options (hash)]) + (define components (for/list ([c report-components]) (c options))) + (report-renderer/stream source name components + (report-options (hash-ref options 'symlink #f) + (hash-ref options 'interval 2.0)) + #f #f)) + + +(struct report-renderer (source name components options) + #:transparent + #:methods gen:renderer + [(define (start-renderer self profile reporter) + (for ([c (report-renderer-components self)]) + (init-component c))) + (define (finish-renderer self profile) + (match-define (report-renderer source name components options) self) + (define events (prune-short-events (reverse (unbox (profile-state-events profile))))) + (define messages + (cons (metadata-message source name) + (apply append (for/list ([c components]) (receive-data c events))))) + (define path (render-html-template source (report-options-symlink? options))) + (render-report-messages messages path) + (open-report-in-browser path))]) + + +(struct report-renderer/stream report-renderer ([shutdown! #:mutable] [channel #:mutable]) + #:transparent + #:methods gen:renderer + [(define (start-renderer self profile reporter) + (match-define (report-renderer/stream source name components options _ _) self) + (for ([c (report-renderer-components self)]) + (init-component c)) + ; launch the server + (define-values (port shutdown! connected-channel) + (start-streaming-server self profile reporter)) + (set-report-renderer/stream-shutdown!! self shutdown!) + (set-report-renderer/stream-channel! self connected-channel) + ; open the browser with the initial messages + (define path (render-html-template source (report-options-symlink? options))) + (define messages + (list (metadata-message source name) + (stream-start-message port))) + (render-report-messages messages path) + (open-report-in-browser path) + ; wait for the browser to open the connection + (match (channel-get connected-channel) + ['connected void] + [x (error "unexpected response from client" x)])) + (define (finish-renderer self profile) + (match-define (report-renderer/stream _ _ _ _ shutdown! channel) self) + ; tell the connected client to wrap up + (channel-put channel 'finish) + ; wait for it to acknowledge, to give it time to pump its last messages + (match (channel-get channel) + ['finish void] + [x (raise x)]) + ; now safe to shutdown the websocker server + (shutdown!))]) + + +; Launch the WebSocket server +(define (start-streaming-server renderer profile reporter) + (match-define (report-renderer/stream _ _ components options _ _) renderer) + (define channel (make-channel)) ; channel for communicating with main thread + (define connected? (box #f)) ; only one client may connect + (define events-box (profile-state-events profile)) + (define interval (report-options-interval options)) + + ; the procedure for handling connections + (define (ws-connection conn state) + ; only one client may connect + (cond + [(box-cas! connected? #f #t) ; we won the race to connect + ; tell the main thread we're connected + (channel-put channel 'connected) + ; loop until we're done + (let loop () + (define sync-result (sync/timeout/enable-break interval channel)) + ; get the events from the profile and empty its buffer + (define events + (reverse + (cons (profile-event-sample (get-current-metrics/call reporter)) + (unbox/replace! events-box '())))) + (define filtered-events (if (null? events) events (prune-short-events events))) + ; get the messages from each component + (define messages + (apply append (for/list ([c components]) (receive-data c filtered-events)))) + ; send the messages; bail out if it fails + (define continue? (not (eq? sync-result 'finish))) + (with-handlers ([exn:fail? (lambda (e) (set! continue? #f))]) + (ws-send! conn (jsexpr->bytes messages))) + (if continue? + (loop) + (begin + (with-handlers ([exn:fail? void]) ; the connection might be dead, but we don't care + (ws-send! conn (jsexpr->bytes (list (stream-finish-message)))) + (ws-close! conn)) + ; if we weren't told to shut down, we need to wait until we are. + (unless (eq? sync-result 'finish) + (channel-get channel)) + (channel-put channel 'finish))))] + [else + (channel-put channel "another client is already connected") + (ws-close! conn)])) + + ; start the server + (define conf-channel (make-async-channel)) + (define server-shutdown! + (ws-serve #:confirmation-channel conf-channel + #:port 8048 + ws-connection)) + ; wait until it's started + (define the-port (async-channel-get conf-channel)) + (unless (number? the-port) + (raise the-port)) + + (values the-port server-shutdown! channel)) + + +;; Remove enter/exit events corresponding to calls that are too short to render. +;; This only removes enter/exit events that contain no intervening events, to +;; avoid pruning "interesting" calls. +(define (prune-short-events events [min% 0.001]) + ; determine the minimum time for an event to be included + (define (event->time evt) + (metrics-ref (profile-event-metrics evt) 'time)) + (define (dt enter exit) + (- (event->time exit) + (event->time enter))) + + ; find the first and last events that have timestamps + (define-values (first last) + (let loop ([first #f][last #f][events events]) + (cond + [(null? events) (values first last)] + [(false? first) + (if (false? (event->time (car events))) + (loop first last (cdr events)) + (loop (car events) (car events) (cdr events)))] + [else + (if (false? (event->time (car events))) + (loop first last (cdr events)) + (loop first (car events) (cdr events)))]))) + + (define MIN_TIME (if (= (filtering-threshold) 0) + 0 + (* (dt first last) min%))) + (define new-events '()) + + (for ([e events]) + (cond + [(and (profile-event-exit? e) + (not (null? new-events))) + (if (and (profile-event-enter? (car new-events)) + (< (dt (car new-events) e) MIN_TIME)) + (set! new-events (cdr new-events)) + (set! new-events (cons e new-events)))] + [else (set! new-events (cons e new-events))])) + + (reverse new-events)) + + +;; Initialize the template +(define (render-html-template source symlink?) + ; set up output directory + (define output-dir + (if symlink? + (build-path (current-directory) "profiles" (make-folder-name source)) + (build-path (find-system-path 'temp-dir) (make-folder-name source)))) + (make-directory* output-dir) + + ; link/copy the template files into the output directory + (define copy-or-symlink (if symlink? + make-file-or-directory-link + copy-directory/files)) + (let ([src (path->complete-path template-dir)]) + (for ([n (list "profile.html" "css" "js")]) + (copy-or-symlink (build-path src n) (build-path output-dir n)))) + + output-dir) + + +;; A special type of message that contains the metadata +(define (metadata-message source name) + (hash 'type "metadata" + 'name name + 'time (parameterize ([date-display-format 'iso-8601]) + (string-replace (date->string (current-date) #t) "T" " ")) + 'source (syntax-srcloc source) + 'form (if (syntax? source) (~a (syntax->datum source)) "") + 'version 1)) + +;; A special type of message that tells the client to connect to a websocker +(define (stream-start-message port) + (hash 'type "stream" + 'event "start" + 'url (format "ws://localhost:~v/" port))) + +;; A special type of message that tells the client to close its websocket +(define (stream-finish-message) + (hash 'type "stream" 'event "finish")) + + +;; Render a list of messages (which must be jsexpr?s) to the data file. +;; They're written in JSONP format, so will invoke the client's receiveData +;; callback when loaded. +(define (render-report-messages messages path) + (let ([out (open-output-file (build-path path "report_data.js"))]) + (fprintf out "data.receiveData(") + (write-json messages out) + (fprintf out ");\n") + (close-output-port out))) + + +;; Open the report in the system browser +(define (open-report-in-browser path) + (define profile-path (build-path path "profile.html")) + (printf "Wrote profile to: ~a\n" profile-path) + (unless (getenv "SYMPRONOOPEN") + (send-url/file profile-path))) + + +; Atomically replace the value in the box with a new value, +; and return the value that was previously there. +(define (unbox/replace! box new) + (define v (unbox box)) + (if (box-cas! box v new) v (unbox/replace! box new))) diff --git a/rosette/lib/profile/renderer/report/callgraph.rkt b/rosette/lib/profile/renderer/report/callgraph.rkt new file mode 100644 index 00000000..f82089c6 --- /dev/null +++ b/rosette/lib/profile/renderer/report/callgraph.rkt @@ -0,0 +1,49 @@ +#lang racket + +(require "generic.rkt" + "../../data.rkt" "../../reporter.rkt" "../../feature.rkt" "../../record.rkt" + "../syntax.rkt") +(provide make-callgraph-component) + +; The callgraph component simply passes through to the client all the events +; needed to construct the call graph. + +(define (make-callgraph-component options) + (callgraph-report-component)) + +(struct callgraph-report-component () #:transparent + #:methods gen:report-component + [(define (init-component self) + void) + (define (receive-data self events) + (list (hash 'type "callgraph" + 'events (events->jsexpr events))))]) + +(define (render-event? event) + (or (profile-event-enter? event) + (profile-event-exit? event) + (profile-event-sample? event))) + + +(define (render-event event) + (match event + [(profile-event-enter met id loc proc in) + (hash 'type "ENTER" + 'id id + 'function (procedure-name proc) + 'callsite (syntax-srcloc loc) + 'source (let ([src (hash-ref (current-sources) proc #f)]) (if src (syntax-srcloc src) src)) + 'inputs (features->flat-hash in) + 'metrics (metrics->hash met))] + [(profile-event-exit met out) + (hash 'type "EXIT" + 'outputs (features->flat-hash out) + 'metrics (metrics->hash met))] + [(profile-event-sample met) + (hash 'type "SAMPLE" + 'metrics (metrics->hash met))] + [_ (error 'render-event "unknown event ~v" event)])) + + +(define (events->jsexpr events) + (map render-event (filter render-event? events))) diff --git a/rosette/lib/profile/renderer/report/generic.rkt b/rosette/lib/profile/renderer/report/generic.rkt new file mode 100644 index 00000000..79d68248 --- /dev/null +++ b/rosette/lib/profile/renderer/report/generic.rkt @@ -0,0 +1,8 @@ +#lang racket + +(require racket/generic) +(provide (all-defined-out)) + +(define-generics report-component + (init-component report-component) + (receive-data report-component profile)) diff --git a/rosette/lib/profile/renderer/report/html/css/d3-stack-graph.css b/rosette/lib/profile/renderer/report/html/css/d3-stack-graph.css new file mode 100644 index 00000000..818c4fe5 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/css/d3-stack-graph.css @@ -0,0 +1,21 @@ +.stack-graph-rect { + stroke: #EEEEEE; +} + +.stack-graph-rect:hover { + stroke: #474747; + stroke-width: 0.8; + cursor: pointer; +} + +.stack-graph-label { + pointer-events: none; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin-left: 4px; + margin-right: 4px; + line-height: 1.5; + padding: 0 0 0; + text-align: left; +} \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/css/profile.css b/rosette/lib/profile/renderer/report/html/css/profile.css new file mode 100644 index 00000000..33259c50 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/css/profile.css @@ -0,0 +1,158 @@ +html { + font-family: Helvetica, Arial, sans-serif; +} + +#header { + padding: 0.5em 1em; + background: #002546; + color: #dddddd; +} +#profile-source { + color: #efefef; + font-weight: 600; +} +#progress { + float: right; + display: none; +} + +#content { + padding: 1em; +} + +.help { + display: inline-block; + background-color: #777777; + color: #efefef; + width: 1em; + height: 1em; + border-radius: 0.5em; + font-weight: normal; + cursor: default; + text-align: center; +} + +.ylabel { + font-size: 16px; +} + +.trendgraph-bar { + fill: #0F5492; +} + +.stack-graph-unzoom { + fill: #002546; +} +.stack-graph-unzoom + * .stack-graph-label { + color: #efefef !important; +} + +.stack-graph-label { + font-size: 12px; + font-family: Helvetica, Arial, sans-serif; + color: black; +} + +.stack-graph-node-highlight { + fill: #0F5492; +} +.stack-graph-node-highlight + * .stack-graph-label { + color: #efefef !important; +} + +#stackgraph { + margin-top: 0.5em; +} + +#calls { + margin-top: 1em; +} + +#calltable { + border-collapse: collapse; + margin-top: 0.5rem; +} + +#calltable th, +#calltable td { + padding: 0.25rem 0.15rem; +} + +#calltable-config { + text-align: left; + font-weight: normal; + font-size: 90%; +} +#calltable-config-more { + margin-top: 0.5rem; + display: none; +} +#calltable-config label + label, +#calltable-config-toggle-more { + margin-left: 1.5rem; +} +#calltable-config-toggle-more, +#calltable-config-toggle-more:active, +#calltable-config-toggle-more:hover, +#calltable-config-toggle-more:visited { + color: #333; +} +#calltable td { + text-align: right; +} +#calltable td.score, +#calltable td.name { + text-align: left; +} +.calltable-highlight, +#calltable tbody tr:hover { + background-color: #E3F2FF; +} +#calltable-score-header { + min-width: 8.3em; +} +#calltable .scorebar { + height: 0.5em; +} +#calltable .scorecell-bar { + width: 6em; + text-align: right; + display: inline-block; +} +#calltable .scorecell-score { + width: 2em; + text-align: right; + display: inline-block; + margin-right: 0.3em; +} +#calltable .score-checkbox { + display: none; +} + +.source, .numcalls, .signature { + font-size: 80%; + color: #aaaaaa; + margin-left: 0.4rem; +} + +ul.context-list { + margin: 0; + font-size: 80%; + padding-left: 1em; + list-style-type: none; + color: #777777; +} +ul.context-list li:before { + content: '↴'; + display: inline-block; + font-size: 100%; + transform: rotate(180deg) translateY(0.3em); + color: #777777; +} + +.tooltip { + background-color: #dddddd; + font-size: 90%; + padding: 0.5rem; + max-width: 22em; +} \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/css/spinner.gif b/rosette/lib/profile/renderer/report/html/css/spinner.gif new file mode 100644 index 00000000..8fa139e8 Binary files /dev/null and b/rosette/lib/profile/renderer/report/html/css/spinner.gif differ diff --git a/rosette/lib/profile/renderer/report/html/css/tablesort.css b/rosette/lib/profile/renderer/report/html/css/tablesort.css new file mode 100644 index 00000000..2e5ff15c --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/css/tablesort.css @@ -0,0 +1,35 @@ +th.sort-header::-moz-selection { + background: transparent; +} +th.sort-header::selection { + background: transparent; +} +th.sort-header { + cursor: pointer; +} +th.sort-header::-moz-selection, +th.sort-header::selection { + background: transparent; +} +table th.sort-header:after { + content: ''; + float: left; + margin-top: 7px; + border-width: 0 4px 4px; + border-style: solid; + border-color: #404040 transparent; + visibility: hidden; +} +table th.sort-header:hover:after { + visibility: visible; +} +table th.sort-up:after, +table th.sort-down:after, +table th.sort-down:hover:after { + visibility: visible; + opacity: 0.4; +} +table th.sort-up:after { + border-bottom: none; + border-width: 4px 4px 0; +} \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/analysis.js b/rosette/lib/profile/renderer/report/html/js/analysis.js new file mode 100644 index 00000000..a058d18e --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/analysis.js @@ -0,0 +1,404 @@ +var analysis; +(function (analysis) { + var options = { + aggregate: false, + signatures: false, + collapseRosette: false, + collapseSolver: false, + columns: [], + contextDepth: 0, + histogramBins: 100 + }; + var analysisCallbacks = []; + var zoomCallbacks = []; + var columnDenominators = {}; + var currentState = null; + var currentZoom = null; + // init + function init() { + data.registerUpdateCallback(receiveDataCallback); + } + analysis.init = init; + function registerAnalysisCallback(cb) { + analysisCallbacks.push(cb); + } + analysis.registerAnalysisCallback = registerAnalysisCallback; + function registerZoomCallback(cb) { + zoomCallbacks.push(cb); + } + analysis.registerZoomCallback = registerZoomCallback; + function setAggregate(x) { + options.aggregate = x; + refresh(); + } + analysis.setAggregate = setAggregate; + function setColumns(x) { + options.columns = x; + refresh(); + } + analysis.setColumns = setColumns; + function setContextDepth(x) { + options.contextDepth = x; + refresh(); + } + analysis.setContextDepth = setContextDepth; + function setHistogramBins(x) { + options.histogramBins = x; + refresh(); + } + analysis.setHistogramBins = setHistogramBins; + function setSignatures(x) { + options.signatures = x; + refresh(); + } + analysis.setSignatures = setSignatures; + function setCollapseRosette(x) { + options.collapseRosette = x; + refresh(); + } + analysis.setCollapseRosette = setCollapseRosette; + function setCollapseSolver(x) { + options.collapseSolver = x; + refresh(); + } + analysis.setCollapseSolver = setCollapseSolver; + function refresh() { + if (currentState) + receiveDataCallback(currentState); + } + analysis.refresh = refresh; + function receiveDataCallback(d) { + // save the state for reuse when options are changed + currentState = d; + // which data to actually use + var root = d.root; + if (options.collapseRosette) { + root = collapseRosetteCalls(root); + } + if (options.collapseSolver) { + root = collapseSolverTime(root, d.solverCalls); + } + // compute scores for the data + var maxScore = computeScores(root); + // compute rows for the table + var rows = computeTableRows(root); + if (!currentZoom) { + currentZoom = root; + } + var a = { + root: root, + solverCalls: d.solverCalls, + rows: rows, + currentZoom: currentZoom, + aggregate: options.aggregate, + maxScore: maxScore + }; + for (var _i = 0, analysisCallbacks_1 = analysisCallbacks; _i < analysisCallbacks_1.length; _i++) { + var cb = analysisCallbacks_1[_i]; + cb(a); + } + } + function zoomTo(root) { + currentZoom = root; + for (var _i = 0, zoomCallbacks_1 = zoomCallbacks; _i < zoomCallbacks_1.length; _i++) { + var cb = zoomCallbacks_1[_i]; + cb(currentZoom); + } + } + analysis.zoomTo = zoomTo; + function computeScores(root) { + if (root === null) + return 0.0; + // first pass: compute denominators + var maxValues = {}; + var nodes = [root]; + while (nodes.length > 0) { + var n = nodes.pop(); + for (var _i = 0, _a = options.columns; _i < _a.length; _i++) { + var c = _a[_i]; + if (n.hasOwnProperty(c.type)) { + var k = c.type + ":" + c.column; + maxValues[k] = Math.max(maxValues[k] || 0, n[c.type][c.column] || 0); + } + } + for (var _b = 0, _c = n.children; _b < _c.length; _b++) { + var c = _c[_b]; + nodes.push(c); + } + } + // second pass: compute scores for each node + nodes.push(root); + var maxScore = 0; + while (nodes.length > 0) { + var n = nodes.pop(); + var score = 0.0; + for (var _d = 0, _e = options.columns; _d < _e.length; _d++) { + var c = _e[_d]; + if (c.score && n.hasOwnProperty(c.type)) { + var k = c.type + ":" + c.column; + if (maxValues[k] > 0) { + score += (n[c.type][c.column] || 0) / maxValues[k]; + } + } + } + n.score = score; + if (score > maxScore) { + maxScore = score; + } + for (var _f = 0, _g = n.children; _f < _g.length; _f++) { + var c = _g[_f]; + nodes.push(c); + } + } + return maxScore; + } + // compute scores for aggregated table rows + function updateScoresForRows(rows) { + if (rows.length == 0) + return rows; + // first pass: compute denominators + var maxValues = []; + for (var _i = 0, rows_1 = rows; _i < rows_1.length; _i++) { + var r = rows_1[_i]; + for (var i = 0; i < r.columns.length; i++) { + if (r.columns[i] >= (maxValues[i] || 0)) { + maxValues[i] = r.columns[i]; + } + } + } + // second pass: compute scores for each row + for (var _a = 0, rows_2 = rows; _a < rows_2.length; _a++) { + var r = rows_2[_a]; + var score = 0.0; + for (var i = 0; i < r.columns.length; i++) { + if (options.columns[i].score && maxValues[i] > 0) { + score += r.columns[i] / maxValues[i]; + } + } + r.score = score; + } + } + // get the key used for aggregating nodes together according to context + function getAggregateKeyForNode(node) { + var context = options.contextDepth; + var key = node.name + "(" + node.source + ")"; + while (context-- != 0 && node.parent) { + node = node.parent; + key = key + "\\" + node.name + "(" + node.source + ")"; + } + if (options.signatures) { + key = (node.inputs["signature"] || []).join("->") + "|" + + (node.outputs["signature"] || []).join("->") + "|" + + key; + } + return key; + } + // compute the analysis rows by aggregating and scoring them + function computeTableRows(root) { + if (root === null) + return []; + if (options.aggregate) { + // group rows by the aggregate key (wrt context) + var nodes = [root]; + var ctxs = {}; + while (nodes.length > 0) { + var n = nodes.pop(); + if (n) { + var k = getAggregateKeyForNode(n); + if (!ctxs.hasOwnProperty(k)) + ctxs[k] = []; + ctxs[k].push(n); + for (var _i = 0, _a = n.children; _i < _a.length; _i++) { + var c = _a[_i]; + nodes.push(c); + } + } + } + // create row for each node + var allRows = []; + for (var k in ctxs) { + var rows = ctxs[k]; + if (rows.length > 0) { + var first = rows[0]; + // compute the row's data as the total within + var maxScore = 0.0; + var totalValues = {}; + for (var _b = 0, rows_3 = rows; _b < rows_3.length; _b++) { + var n = rows_3[_b]; + for (var _c = 0, _d = options.columns; _c < _d.length; _c++) { + var c = _d[_c]; + if (n.hasOwnProperty(c.type)) { + var k_1 = c.type + ":" + c.column; + totalValues[k_1] = (totalValues[k_1] || 0) + (n[c.type][c.column] || 0); + } + } + maxScore = Math.max(maxScore, n.score); + } + var columns = []; + for (var _e = 0, _f = options.columns; _e < _f.length; _e++) { + var k_2 = _f[_e]; + columns.push(totalValues[k_2.type + ":" + k_2.column]); + } + var row = { + function: first.name, + node: first, + allNodes: rows, + score: maxScore, + columns: columns + }; + allRows.push(row); + } + } + updateScoresForRows(allRows); // this should really be an option + return allRows; + } + else { + // create a row for each call + var nodes = [root]; + var rows = []; + while (nodes.length > 0) { + var n = nodes.pop(); + if (!n) + continue; + var values = {}; + for (var _g = 0, _h = options.columns; _g < _h.length; _g++) { + var c = _h[_g]; + if (n.hasOwnProperty(c.type)) { + var k = c.type + ":" + c.column; + values[k] = n[c.type][c.column] || 0; + } + } + var columns = []; + for (var _j = 0, _k = options.columns; _j < _k.length; _j++) { + var k = _k[_j]; + columns.push(values[k.type + ":" + k.column]); + } + var row = { + function: n.name, + node: n, + allNodes: [n], + score: n.score, + columns: columns + }; + rows.push(row); + for (var _l = 0, _m = n.children; _l < _m.length; _l++) { + var c = _m[_l]; + nodes.push(c); + } + } + return rows; + } + } + function collapseRosetteCalls(root) { + var rec = function (node) { + var newExcl = undefined; + var newChildren = []; + var modified = false; + for (var _i = 0, _a = node.children; _i < _a.length; _i++) { + var c = _a[_i]; + var newC = rec(c); // recurse to collapse children + if (newC.name[0] == "@") { + if (typeof newExcl === "undefined") { + newExcl = {}; + for (var _b = 0, _c = Object.keys(node.excl); _b < _c.length; _b++) { + var k = _c[_b]; + newExcl[k] = node.excl[k]; + } + } + for (var _d = 0, _e = Object.keys(newC.excl); _d < _e.length; _d++) { + var k = _e[_d]; + newExcl[k] = (newExcl[k] || 0) + newC.excl[k]; // add all c's children + } + for (var _f = 0, _g = newC.children; _f < _g.length; _f++) { + var cc = _g[_f]; + newChildren.push(cc); + } + modified = true; + } + else { + if (newC !== c) { + modified = true; + } + newChildren.push(newC); // recurse + } + } + if (modified) { + var newNode = {}; + for (var _h = 0, _j = Object.keys(node); _h < _j.length; _h++) { + var k = _j[_h]; + newNode[k] = node[k]; + } + if (typeof newExcl !== "undefined") { + newNode.excl = newExcl; + } + newNode.children = newChildren; + return newNode; + } + else { + return node; + } + }; + var newRoot = rec(root); + return newRoot; + } + // remove all solver time from exclusive time + function collapseSolverTime(root, solverCalls) { + var rec = function (node) { + var exclDt = 0; + var newChildren = []; + var modified = false; + var start = node.start; + // do the spaces before each child + for (var _i = 0, _a = node.children; _i < _a.length; _i++) { + var c = _a[_i]; + var finish_1 = c.start; + for (var _b = 0, solverCalls_1 = solverCalls; _b < solverCalls_1.length; _b++) { + var sc = solverCalls_1[_b]; + var scfinish = typeof sc.finish === "undefined" ? root.finish : sc.finish; + if (scfinish < start) + continue; + if (finish_1 < sc.start) + break; // todo make not quadratic + var delta = Math.min(finish_1, scfinish) - Math.max(start, sc.start); + exclDt += delta; + } + var ret = rec(c); + newChildren.push(ret); + if (ret !== c) + modified = true; + start = c.finish; + } + // do the space between last child and my end + var finish = node.finish; + for (var _c = 0, solverCalls_2 = solverCalls; _c < solverCalls_2.length; _c++) { + var sc = solverCalls_2[_c]; + var scfinish = typeof sc.finish === "undefined" ? root.finish : sc.finish; + if (scfinish < start) + continue; + if (finish < sc.start) + break; + var delta = Math.min(finish, scfinish) - Math.max(start, sc.start); + exclDt += delta; + } + if (exclDt > 0 || modified) { + var newNode = {}; + for (var _d = 0, _e = Object.keys(node); _d < _e.length; _d++) { + var k = _e[_d]; + newNode[k] = node[k]; + } + var newExcl = {}; + for (var _f = 0, _g = Object.keys(node.excl); _f < _g.length; _f++) { + var k = _g[_f]; + newExcl[k] = node.excl[k]; + } + newNode.excl = newExcl; + newNode.excl["time"] -= exclDt; + newNode.children = newChildren; + node = newNode; + } + return node; + }; + return rec(root); + } +})(analysis || (analysis = {})); +//# sourceMappingURL=analysis.js.map \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/analysis.ts b/rosette/lib/profile/renderer/report/html/js/analysis.ts new file mode 100644 index 00000000..db8b56fb --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/analysis.ts @@ -0,0 +1,406 @@ +namespace analysis { + import CallNode = data.CallNode; + import SolverCall = data.SolverCall; + + type AnalysisCallback = (d: ProfileData) => void; + type ZoomCallback = (d: CallNode) => void; + + export interface ProfileData { + root: CallNode; + solverCalls: SolverCall[]; + rows: AnalysisRow[]; + currentZoom: CallNode; + aggregate: boolean; + maxScore: number; + } + + export interface AnalysisRow { + function: string; + node: CallNode; + allNodes: CallNode[]; + score: number; + columns: number[]; + } + + export interface AnalysisColumn { + type: string; + column: string; + name: string; + score: boolean; + description?: string; + } + + var options = { + aggregate: false, + signatures: false, + collapseRosette: false, + collapseSolver: false, + columns: [], + contextDepth: 0, + histogramBins: 100 + }; + + var analysisCallbacks: AnalysisCallback[] = []; + var zoomCallbacks: ZoomCallback[] = []; + var columnDenominators: Map = {}; + var currentState: data.ProfileState = null; + var currentZoom: CallNode = null; + + // init + export function init() { + data.registerUpdateCallback(receiveDataCallback); + } + + export function registerAnalysisCallback(cb: AnalysisCallback): void { + analysisCallbacks.push(cb); + } + + export function registerZoomCallback(cb: ZoomCallback): void { + zoomCallbacks.push(cb); + } + + export function setAggregate(x: boolean): void { + options.aggregate = x; + refresh(); + } + export function setColumns(x: AnalysisColumn[]): void { + options.columns = x; + refresh(); + } + export function setContextDepth(x: number): void { + options.contextDepth = x; + refresh(); + } + export function setHistogramBins(x: number): void { + options.histogramBins = x; + refresh(); + } + export function setSignatures(x: boolean): void { + options.signatures = x; + refresh(); + } + export function setCollapseRosette(x: boolean): void { + options.collapseRosette = x; + refresh(); + } + export function setCollapseSolver(x: boolean): void { + options.collapseSolver = x; + refresh(); + } + + export function refresh(): void { + if (currentState) receiveDataCallback(currentState); + } + + function receiveDataCallback(d: data.ProfileState): void { + // save the state for reuse when options are changed + currentState = d; + + // which data to actually use + let root: CallNode = d.root; + if (options.collapseRosette) { + root = collapseRosetteCalls(root); + } + if (options.collapseSolver) { + root = collapseSolverTime(root, d.solverCalls); + } + + // compute scores for the data + let maxScore = computeScores(root); + + // compute rows for the table + let rows = computeTableRows(root); + + if (!currentZoom) { + currentZoom = root; + } + + let a: ProfileData = { + root: root, + solverCalls: d.solverCalls, + rows: rows, + currentZoom: currentZoom, + aggregate: options.aggregate, + maxScore: maxScore + }; + + for (let cb of analysisCallbacks) { + cb(a); + } + } + + + export function zoomTo(root: CallNode) { + currentZoom = root; + for (let cb of zoomCallbacks) { + cb(currentZoom); + } + } + + + function computeScores(root: CallNode): number { + if (root === null) return 0.0; + + // first pass: compute denominators + let maxValues: Map = {}; + let nodes = [root]; + while (nodes.length > 0) { + let n = nodes.pop()!; + for (let c of options.columns) { + if (n.hasOwnProperty(c.type)) { + let k = c.type + ":" + c.column; + maxValues[k] = Math.max(maxValues[k] || 0, n[c.type][c.column] || 0); + } + } + for (let c of n.children) nodes.push(c); + } + // second pass: compute scores for each node + nodes.push(root); + let maxScore = 0; + while (nodes.length > 0) { + let n = nodes.pop()!; + let score = 0.0; + for (let c of options.columns) { + if (c.score && n.hasOwnProperty(c.type)) { + let k = c.type + ":" + c.column; + if (maxValues[k] > 0) { + score += (n[c.type][c.column] || 0) / maxValues[k]; + } + } + } + n.score = score; + if (score > maxScore) { + maxScore = score; + } + for (let c of n.children) nodes.push(c); + } + return maxScore; + } + + + // compute scores for aggregated table rows + function updateScoresForRows(rows: AnalysisRow[]) { + if (rows.length == 0) return rows; + + // first pass: compute denominators + let maxValues: number[] = []; + for (let r of rows) { + for (let i = 0; i < r.columns.length; i++) { + if (r.columns[i] >= (maxValues[i] || 0)) { + maxValues[i] = r.columns[i]; + } + } + } + + // second pass: compute scores for each row + for (let r of rows) { + let score = 0.0; + for (let i = 0; i < r.columns.length; i++) { + if (options.columns[i].score && maxValues[i] > 0) { + score += r.columns[i] / maxValues[i]; + } + } + r.score = score; + } + } + + + // get the key used for aggregating nodes together according to context + function getAggregateKeyForNode(node: CallNode): string { + var context = options.contextDepth; + var key = node.name + "(" + node.source + ")"; + while (context-- != 0 && node.parent) { + node = node.parent; + key = key + "\\" + node.name + "(" + node.source + ")"; + } + if (options.signatures) { + key = (node.inputs["signature"] || []).join("->") + "|" + + (node.outputs["signature"] || []).join("->") + "|" + + key; + } + return key; + } + + + // compute the analysis rows by aggregating and scoring them + function computeTableRows(root: CallNode): AnalysisRow[] { + if (root === null) return []; + + if (options.aggregate) { + // group rows by the aggregate key (wrt context) + let nodes = [root]; + let ctxs: Map = {}; + while (nodes.length > 0) { + let n = nodes.pop()!; + if (n) { + let k = getAggregateKeyForNode(n); + if (!ctxs.hasOwnProperty(k)) ctxs[k] = []; + ctxs[k].push(n); + for (let c of n.children) nodes.push(c); + } + } + // create row for each node + let allRows: AnalysisRow[] = []; + for (let k in ctxs) { + let rows = ctxs[k]; + if (rows.length > 0) { + let first = rows[0]!; + // compute the row's data as the total within + let maxScore = 0.0; + let totalValues: Map = {}; + for (let n of rows) { + for (let c of options.columns) { + if (n.hasOwnProperty(c.type)) { + let k = c.type + ":" + c.column; + totalValues[k] = (totalValues[k] || 0) + (n[c.type][c.column] || 0); + } + } + maxScore = Math.max(maxScore, n.score); + } + let columns = []; + for (let k of options.columns) { + columns.push(totalValues[k.type + ":" + k.column]); + } + let row: AnalysisRow = { + function: first.name, + node: first, + allNodes: rows, + score: maxScore, + columns: columns + }; + allRows.push(row); + } + } + updateScoresForRows(allRows); // this should really be an option + return allRows; + } else { + // create a row for each call + let nodes = [root]; + let rows: AnalysisRow[] = []; + while (nodes.length > 0) { + let n = nodes.pop()!; + if (!n) continue; + let values: Map = {}; + for (let c of options.columns) { + if (n.hasOwnProperty(c.type)) { + let k = c.type + ":" + c.column; + values[k] = n[c.type][c.column] || 0; + } + } + let columns = []; + for (let k of options.columns) { + columns.push(values[k.type + ":" + k.column]); + } + let row: AnalysisRow = { + function: n.name, + node: n, + allNodes: [n], + score: n.score, + columns: columns + }; + rows.push(row); + for (let c of n.children) nodes.push(c); + } + return rows; + } + } + + + function collapseRosetteCalls(root: CallNode): CallNode { + let rec = (node: CallNode): CallNode => { + let newExcl = undefined; + let newChildren = []; + let modified = false; + for (let c of node.children) { + let newC = rec(c); // recurse to collapse children + if (newC.name[0] == "@") { + if (typeof newExcl === "undefined") { + newExcl = {}; + for (let k of Object.keys(node.excl)) { + newExcl[k] = node.excl[k]; + } + } + for (let k of Object.keys(newC.excl)) { + newExcl[k] = (newExcl[k] || 0) + newC.excl[k]; // add all c's children + } + for (let cc of newC.children) { + newChildren.push(cc); + } + modified = true; + } else { + if (newC !== c) { + modified = true; + } + newChildren.push(newC); // recurse + } + } + if (modified) { + let newNode = {}; + for (let k of Object.keys(node)) { + newNode[k] = node[k]; + } + if (typeof newExcl !== "undefined") { + (newNode).excl = newExcl; + } + (newNode).children = newChildren; + return newNode; + } else { + return node; + } + } + let newRoot = rec(root); + return newRoot; + } + + + // remove all solver time from exclusive time + function collapseSolverTime(root: CallNode, solverCalls: SolverCall[]): CallNode { + let rec = (node: CallNode) => { + let exclDt = 0; + let newChildren = []; + let modified = false; + let start = node.start; + // do the spaces before each child + for (let c of node.children) { + let finish = c.start; + for (let sc of solverCalls) { + let scfinish = typeof sc.finish === "undefined" ? root.finish : sc.finish; + if (scfinish < start) continue; + if (finish < sc.start) break; // todo make not quadratic + let delta = Math.min(finish, scfinish) - Math.max(start, sc.start); + exclDt += delta; + } + let ret = rec(c); + newChildren.push(ret); + if (ret !== c) modified = true; + start = c.finish; + } + // do the space between last child and my end + let finish = node.finish; + for (let sc of solverCalls) { + let scfinish = typeof sc.finish === "undefined" ? root.finish : sc.finish; + if (scfinish < start) continue; + if (finish < sc.start) break; + let delta = Math.min(finish, scfinish) - Math.max(start, sc.start); + exclDt += delta; + } + if (exclDt > 0 || modified) { + let newNode = {}; + for (let k of Object.keys(node)) { + newNode[k] = node[k]; + } + let newExcl = {}; + for (let k of Object.keys(node.excl)) { + newExcl[k] = node.excl[k]; + } + (newNode).excl = newExcl; + (newNode).excl["time"] -= exclDt; + (newNode).children = newChildren; + node = (newNode); + } + return node; + } + + return rec(root); + } +} \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/d3-stack-graph.js b/rosette/lib/profile/renderer/report/html/js/d3-stack-graph.js new file mode 100644 index 00000000..273b6056 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/d3-stack-graph.js @@ -0,0 +1,395 @@ +var d3_stackgraph; +(function (d3_stackgraph) { + var PADDING_X_LEFT = 30; + var PADDING_X_RIGHT = 20; + var PADDING_Y_BOTTOM = 18; + var X_AXIS_TICKS = 10; + var MIN_WIDTH = 0.5; // px + var TOOLTIP_KEY = "stackGraphTooltip"; + var StackGraphNode = /** @class */ (function () { + function StackGraphNode(data) { + this.start = 0; + this.finish = 1; + this.depth = 0; // root has depth 0 + this.data = data; + this.children = []; + } + StackGraphNode.prototype.descendants = function () { + var nodes = [this]; + var descs = []; + while (nodes.length > 0) { + var node = nodes.pop(); + descs.push(node); + for (var _i = 0, _a = node.children; _i < _a.length; _i++) { + var c = _a[_i]; + nodes.push(c); + } + } + return descs; + }; + return StackGraphNode; + }()); + var unzoomData = { + start: 0, finish: 0, parent: null, children: [], name: "", id: "unzoom" + }; + var StackGraph = /** @class */ (function () { + function StackGraph(s) { + this._width = 960; + this._cellHeight = 18; + this._transitionDuration = 750; + this._transitionEase = d3.easeCubic; + this._data = null; + this._highlights = []; + this._color = function (d) { return "#00ff00"; }; + this._textColor = function (d) { return "#000000"; }; + this._discontinuities = []; + this.zoomed = false; + this.nextId = 0; + this.idToNode = {}; + this.mouseoverCallback = null; + this.mouseoutCallback = null; + this.selector = s; + } + StackGraph.prototype.width = function (x) { + if (typeof x === "undefined") + return this._width; + this._width = x; + // this.render(true); + return this; + }; + StackGraph.prototype.height = function (x) { + if (typeof x === "undefined") + return this._height; + this._height = x; + // this.render(true); + return this; + }; + StackGraph.prototype.cellHeight = function (x) { + if (typeof x === "undefined") + return this._cellHeight; + this._cellHeight = x; + // this.render(true); + return this; + }; + StackGraph.prototype.clickHandler = function (x) { + if (typeof x === "undefined") + return this._clickHandler; + this._clickHandler = x; + return this; + }; + StackGraph.prototype.hoverHandler = function (x) { + if (typeof x === "undefined") + return this._hoverHandler; + this._hoverHandler = x; + return this; + }; + StackGraph.prototype.data = function (x) { + if (typeof x === "undefined") + return this._data; + this._data = x; + if (!this.zoomed) { + this.root = this.partition(x); + } + return this; + }; + StackGraph.prototype.highlights = function (x) { + if (typeof x === "undefined") + return this._highlights; + this._highlights = x; + return this; + }; + StackGraph.prototype.color = function (x) { + if (typeof x === "undefined") + return this._color; + this._color = x; + return this; + }; + StackGraph.prototype.textColor = function (x) { + if (typeof x === "undefined") + return this._textColor; + this._textColor = x; + return this; + }; + StackGraph.prototype.discontinuities = function (x) { + if (typeof x === "undefined") + return this._discontinuities; + this._discontinuities = x; + return this; + }; + StackGraph.prototype.clickCallback = function (d) { + if (!this._clickHandler) + return; + if (d.data === unzoomData) { + this._clickHandler(this._data); + } + else if (d.data !== this._data) { + this._clickHandler(d.data); + } + }; + StackGraph.prototype.zoom = function (d) { + if (d === this._data) { + this.zoomed = false; + this.root = this.partition(this._data); + this.render(true); + } + else { + this.zoomed = true; + this.root = this.partition(d); + this.render(true); + } + }; + StackGraph.prototype.highlightData = function (x) { + var _this = this; + // map the data to their corresponding nodes; remove those that don't exist + var nodes = x.map(function (d) { return _this.idToNode[d.id]; }).filter(function (d) { return d; }); + var g = this.svg.selectAll("g.stack-graph-node").data(nodes, function (d) { return d.data.id; }); + // highlight selected nodes + g.select("rect").classed("stack-graph-node-highlight", true); + // unhighlight all other nodes + g.exit().select("rect").classed("stack-graph-node-highlight", false); + }; + StackGraph.prototype.partition = function (rootData) { + this.idToNode = {}; + // first pass: build and layout the hierarchy + var root = new StackGraphNode(rootData); + root.start = rootData.start; + root.finish = rootData.finish; + this.idToNode[rootData.id] = root; + var nodes = [root]; + var maxHeight = 0; + while (nodes.length > 0) { + var node = nodes.pop(); + if (!node.data.id) { + node.data.id = this.nextId.toString(); + this.nextId += 1; + } + for (var _i = 0, _a = node.data.children; _i < _a.length; _i++) { + var c = _a[_i]; + var cn = new StackGraphNode(c); + cn.parent = node; + cn.depth = node.depth + 1; + cn.start = c.start; + cn.finish = c.finish; + if (cn.depth > maxHeight) + maxHeight = cn.depth; + this.idToNode[cn.data.id] = cn; + node.children.push(cn); + nodes.push(cn); + } + } + // finally set height if it's not default + var computedHeight = Math.max(maxHeight + 1, 5) * this._cellHeight + PADDING_Y_BOTTOM; + if (!this._height || (rootData === this._data && this._height > computedHeight)) { + this._height = computedHeight; + } + return root; + }; + StackGraph.prototype.setupSvg = function () { + this.svg = d3.select(this.selector).append('svg').attr("class", "stack-graph"); + this.svg.append("g").attr("class", "stack-graph-highlights"); + this.svg.append("g").attr("class", "stack-graph-body"); + var labels = this.svg.append("g").attr("class", "stack-graph-labels"); + labels.append("rect").attr("class", "stack-graph-label-bg").attr("fill", "#ffffff"); + labels.append("text").attr("text-anchor", "middle") + .attr("transform", "rotate(270)") + .text("Call Stack"); + var axes = this.svg.append("g").attr("class", "stack-graph-axis"); + axes.append("rect").attr("class", "stack-graph-axis-bg").attr("fill", "#ffffff"); + this.svg.append("g").attr("class", "stack-graph-breaks"); + var this_ = this; + var hasClass = function (elt, id) { + if (elt.classList) + return elt.classList.contains(id); + else if (elt.className.baseVal) + return elt.className.baseVal.indexOf(id) > -1; + else + return elt.className.indexOf(id) > -1; + }; + var mouseOpCallback = function (mouseover) { + return function (d, i, x) { + var elt = this; + if (this_._hoverHandler) { + if (hasClass(elt, "stack-graph-highlight") || hasClass(elt, "stack-graph-break")) { + if (mouseover) + tooltip.show(d.summary, elt, "top"); + else + tooltip.hide(); + } + else if (hasClass(elt, "stack-graph-node")) { + this_._hoverHandler(d.data, mouseover); + } + } + }; + }; + this.mouseoverCallback = mouseOpCallback(true); + this.mouseoutCallback = mouseOpCallback(false); + }; + StackGraph.prototype.render = function (anim) { + var _this = this; + if (anim === void 0) { anim = false; } + // set up SVG + if (!this.svg) { + this.setupSvg(); + } + this.svg.attr('width', this._width) + .attr('height', this._height); + var data = this.root ? this.root.descendants() : []; + if (this.zoomed) { + var curr = this.root.data, route = []; + while (curr = curr.parent) + route.push(curr.name); + unzoomData.name = " " + route.join(" ← "); + var unzoomNode = new StackGraphNode(unzoomData); + unzoomNode.start = this.root.start; + unzoomNode.finish = this.root.finish; + unzoomNode.depth = -1; + data.splice(0, 0, unzoomNode); + } + /////////////////////////////////////////////////////////////////// + // update y label position + var gLabel = this.svg.select("g.stack-graph-labels"); + gLabel.select("rect").attr("x", 0) + .attr("y", 0) + .attr("width", PADDING_X_LEFT) + .attr("height", this._height); + gLabel.select("text").attr("dy", PADDING_X_LEFT / 2) + .attr("dx", -this._height / 2); + /////////////////////////////////////////////////////////////////// + // create axes + var xBase = d3.scaleLinear().range([PADDING_X_LEFT, this._width - PADDING_X_RIGHT]); + if (this.root) { + xBase.domain([this.root.start, this.root.finish]); + } + var _a = xBase.domain(), xMin = _a[0], xMax = _a[1]; + var xBaseTo01 = d3.scaleLinear().domain([xMin, xMax]); + var x = fc.scaleDiscontinuous(xBase); + var dcRange = fc.discontinuityRange.apply(fc, this._discontinuities); + x.discontinuityProvider(dcRange); + var xTo01 = fc.scaleDiscontinuous(xBaseTo01); + xTo01.discontinuityProvider(dcRange); + var ticks = d3.ticks(0, 1, X_AXIS_TICKS); + var tickValues = ticks.map(xTo01.invert); + var y = d3.scaleLinear().range([0, this._cellHeight]); + var xAxis = d3.axisBottom(x).tickValues(tickValues); + xAxis.tickFormat(function (x) { return (x / 1000).toFixed(3) + "s"; }); + var xAxisPos = this._height - PADDING_Y_BOTTOM; + this.svg.select("g.stack-graph-axis") + .attr("transform", "translate(0," + xAxisPos + ")") + .call(xAxis); + this.svg.select("rect.stack-graph-axis-bg") + .attr("x", 0).attr("y", 0).attr("width", this._width).attr("height", PADDING_Y_BOTTOM); + var width = function (d) { return x(d.finish) - x(d.start); }; + /////////////////////////////////////////////////////////////////// + // render highlights + var gHigh = this.svg.select("g.stack-graph-highlights"); + var hRect = gHigh.selectAll("rect").data(this._highlights.filter(function (d) { return width(d) > 0; })); + var hEnter = hRect.enter().append("svg:rect").attr("class", "stack-graph-highlight"); + hEnter.on("mouseover", this.mouseoverCallback) + .on("mouseout", this.mouseoutCallback); + hRect.merge(hEnter) + .attr("height", this._height) + .attr("fill", function (d) { return d.color; }) + .attr("title", function (d) { return d.summary; }); + // animate the highlights if requested + var hPos = function (grp) { return grp.attr("width", width) + .attr("transform", function (d) { return "translate(" + x(d.start) + ",0)"; }); }; + if (anim) { + hPos(hRect.transition() + .duration(this._transitionDuration).ease(this._transitionEase)); + } + else { + hPos(hRect); + } + hPos(hEnter); + hRect.exit().remove(); + /////////////////////////////////////////////////////////////////// + // render breaks + var gBreak = this.svg.select("g.stack-graph-breaks"); + var bLine = gBreak.selectAll("line").data(this._highlights.filter(function (d) { return width(d) == 0; })); + var bEnter = bLine.enter().append("svg:line").attr("class", "stack-graph-break"); + bEnter.on("mouseover", this.mouseoverCallback) + .on("mouseout", this.mouseoutCallback); + bLine.merge(bEnter) + .attr("y1", this._height - PADDING_Y_BOTTOM + xAxis.tickSizeInner()) + .attr("y2", 0) + .attr("x1", 0) + .attr("x2", 0) + .attr("stroke-width", 1) + .attr("stroke", "#444444") + .style("stroke-dasharray", "5 5"); + // animate the breaks if requested + var bPos = function (grp) { return grp.attr("transform", function (d) { return "translate(" + x(d.start) + ",0)"; }); }; + if (anim) { + bPos(bLine.transition() + .duration(this._transitionDuration).ease(this._transitionEase)); + } + else { + bPos(bLine); + } + bPos(bEnter); + bLine.exit().remove(); + /////////////////////////////////////////////////////////////////// + // render call stacks + var dx = function (d) { return x(d.start); }; + var dy = function (d) { return _this._height - y(d.depth + (_this.zoomed ? 1 : 0)) - _this._cellHeight - PADDING_Y_BOTTOM; }; + var pos = function (d) { return "translate(" + dx(d) + "," + dy(d) + ")"; }; + var rectClass = function (d) { return d.data === unzoomData ? "stack-graph-rect stack-graph-unzoom" : "stack-graph-rect"; }; + var filteredData = data.filter(function (d) { return width(d) >= MIN_WIDTH; }); + var g = this.svg.select("g.stack-graph-body") + .selectAll("g.stack-graph-node").data(filteredData, function (d) { return d.data.id; }); + var enter = g.enter().append("svg:g"); + enter.append("svg:rect"); + enter.append("foreignObject") + .append("xhtml:div"); + enter.on("click", function (d) { return _this.clickCallback(d); }) + .on("mouseover", this.mouseoverCallback) + .on("mouseout", this.mouseoutCallback); + g.merge(enter) + .attr("height", this._cellHeight) + .attr("name", function (d) { return d.data.name; }) + .attr("class", "stack-graph-node") + .select("rect") + .attr("height", this._cellHeight) + .attr("class", rectClass) + .attr("fill", function (d) { return _this._color(d.data); }); + g.merge(enter) + .select("foreignObject") + .attr("width", width) + .attr("height", this._cellHeight) + .select("div") + .attr("class", "stack-graph-label") + .style("display", function (d) { return width(d) < 35 ? "none" : "block"; }) + .style("color", function (d) { return _this._textColor(d.data); }) + .text(function (d) { return d.data.name; }); + // animate preexisting nodes into new positions + var gPos = function (grp) { return grp.attr("transform", pos) + .attr("width", width) + .select("rect") + .attr("width", width); }; + if (anim) { + gPos(g.transition() + .duration(this._transitionDuration).ease(this._transitionEase)); + } + else { + gPos(g); + } + // don't animate new nodes into position... + gPos(enter); + // but maybe fade them in if this isn't the first render (i.e., if unzooming) + if (anim) { + enter.style("opacity", 0.0) + .transition() + .duration(this._transitionDuration).ease(this._transitionEase) + .style("opacity", 1.0); + } + g.exit().remove(); + }; + return StackGraph; + }()); + d3_stackgraph.StackGraph = StackGraph; + function stackGraph(selector) { + return new StackGraph(selector); + } + d3_stackgraph.stackGraph = stackGraph; +})(d3_stackgraph || (d3_stackgraph = {})); +//# sourceMappingURL=d3-stack-graph.js.map \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/d3-stack-graph.ts b/rosette/lib/profile/renderer/report/html/js/d3-stack-graph.ts new file mode 100644 index 00000000..bce06859 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/d3-stack-graph.ts @@ -0,0 +1,460 @@ +declare var d3; +declare var fc; + +namespace d3_stackgraph { + type ClickHandler = (d: StackGraphData) => any; + type HoverHandler = (d: StackGraphData, enter: boolean) => any; + type ColorHandler = (d: StackGraphData) => string; + type D3Selection = any; + + const PADDING_X_LEFT = 30; + const PADDING_X_RIGHT = 20; + const PADDING_Y_BOTTOM = 18; + const X_AXIS_TICKS = 10; + const MIN_WIDTH = 0.5; // px + + const TOOLTIP_KEY = "stackGraphTooltip"; + + class StackGraphNode { + data: StackGraphData; + start: number = 0; + finish: number = 1; + parent: StackGraphNode; + children: StackGraphNode[]; + depth: number = 0; // root has depth 0 + + constructor(data: StackGraphData) { + this.data = data; + this.children = []; + } + + descendants(): StackGraphNode[] { + let nodes: StackGraphNode[] = [this]; + let descs = []; + while (nodes.length > 0) { + let node = nodes.pop(); + descs.push(node); + for (let c of node.children) nodes.push(c); + } + return descs; + } + } + + export interface StackGraphData { + start: number; + finish: number; + parent: StackGraphData; + children: StackGraphData[]; + name: string; + id: string; + } + + let unzoomData: StackGraphData = { + start: 0, finish: 0, parent: null, children: [], name: "", id: "unzoom" + }; + + export interface StackGraphHighlight { + start: number; + finish: number; + color: string; + summary: string; + } + + export class StackGraph { + private _width: number = 960; + private _height: number; + private _cellHeight: number = 18; + private _selection: D3Selection; + private _transitionDuration: number = 750; + private _transitionEase: any = d3.easeCubic; + private _clickHandler: ClickHandler; + private _hoverHandler: HoverHandler; + private _data: StackGraphData = null; + private _highlights: StackGraphHighlight[] = []; + private _color: ColorHandler = (d) => "#00ff00"; + private _textColor: ColorHandler = (d) => "#000000"; + private _discontinuities: number[][] = []; + + private root: StackGraphNode; + private svg: any; + private selector: string; + private zoomed: boolean = false; + private nextId: number = 0; + private idToNode: Map = {}; + private mouseoverCallback: (d: Object, i: number, x: Object[]) => void = null; + private mouseoutCallback: (d: Object, i: number, x: Object[]) => void = null; + + width(): number + width(x: number): StackGraph + width(x?: number): number | StackGraph { + if (typeof x === "undefined") return this._width; + this._width = x; + // this.render(true); + return this; + } + height(): number + height(x: number): StackGraph + height(x?: number): number | StackGraph { + if (typeof x === "undefined") return this._height; + this._height = x; + // this.render(true); + return this; + } + cellHeight(): number + cellHeight(x: number): StackGraph + cellHeight(x?: number): number | StackGraph { + if (typeof x === "undefined") return this._cellHeight; + this._cellHeight = x; + // this.render(true); + return this; + } + clickHandler(): ClickHandler + clickHandler(x: ClickHandler): StackGraph + clickHandler(x?: ClickHandler): ClickHandler | StackGraph { + if (typeof x === "undefined") return this._clickHandler; + this._clickHandler = x; + return this; + } + hoverHandler(): HoverHandler + hoverHandler(x: HoverHandler): StackGraph + hoverHandler(x?: HoverHandler): HoverHandler | StackGraph { + if (typeof x === "undefined") return this._hoverHandler; + this._hoverHandler = x; + return this; + } + data(): StackGraphData + data(x: StackGraphData): StackGraph + data(x?: StackGraphData): StackGraphData | StackGraph { + if (typeof x === "undefined") return this._data; + this._data = x; + if (!this.zoomed) { + this.root = this.partition(x); + } + return this; + } + highlights(): StackGraphHighlight[] + highlights(x: StackGraphHighlight[]): StackGraph + highlights(x?: StackGraphHighlight[]): StackGraphHighlight[] | StackGraph { + if (typeof x === "undefined") return this._highlights; + this._highlights = x; + return this; + } + color(): ColorHandler + color(x: ColorHandler): StackGraph + color(x?: ColorHandler): ColorHandler | StackGraph { + if (typeof x === "undefined") return this._color; + this._color = x; + return this; + } + textColor(): ColorHandler + textColor(x: ColorHandler): StackGraph + textColor(x?: ColorHandler): ColorHandler | StackGraph { + if (typeof x === "undefined") return this._textColor; + this._textColor = x; + return this; + } + discontinuities(): number[][] + discontinuities(x: number[][]): StackGraph + discontinuities(x?: number[][]): number[][] | StackGraph { + if (typeof x === "undefined") return this._discontinuities; + this._discontinuities = x; + return this; + } + + clickCallback(d: StackGraphNode) { + if (!this._clickHandler) return; + + if (d.data === unzoomData) { + this._clickHandler(this._data); + } else if (d.data !== this._data) { + this._clickHandler(d.data); + } + } + + zoom(d: StackGraphData) { + if (d === this._data) { + this.zoomed = false; + this.root = this.partition(this._data); + this.render(true); + } else { + this.zoomed = true; + this.root = this.partition(d); + this.render(true); + } + } + + highlightData(x: StackGraphData[]) { + // map the data to their corresponding nodes; remove those that don't exist + let nodes = x.map(d => this.idToNode[d.id]).filter(d => d); + + let g = this.svg.selectAll("g.stack-graph-node").data(nodes, (d) => d.data.id); + // highlight selected nodes + g.select("rect").classed("stack-graph-node-highlight", true); + // unhighlight all other nodes + g.exit().select("rect").classed("stack-graph-node-highlight", false); + } + + partition(rootData: StackGraphData): StackGraphNode { + this.idToNode = {}; + // first pass: build and layout the hierarchy + let root = new StackGraphNode(rootData); + root.start = rootData.start; + root.finish = rootData.finish; + this.idToNode[rootData.id] = root; + let nodes = [root]; + var maxHeight = 0; + while (nodes.length > 0) { + let node = nodes.pop(); + if (!node.data.id) { + node.data.id = this.nextId.toString(); + this.nextId += 1; + } + for (let c of node.data.children) { + let cn = new StackGraphNode(c); + cn.parent = node; + cn.depth = node.depth + 1; + cn.start = c.start; + cn.finish = c.finish; + if (cn.depth > maxHeight) maxHeight = cn.depth; + this.idToNode[cn.data.id] = cn; + node.children.push(cn); + nodes.push(cn); + } + } + // finally set height if it's not default + let computedHeight = Math.max(maxHeight + 1, 5) * this._cellHeight + PADDING_Y_BOTTOM; + if (!this._height || (rootData === this._data && this._height > computedHeight)) { + this._height = computedHeight; + } + return root; + } + + setupSvg() { + this.svg = d3.select(this.selector).append('svg').attr("class", "stack-graph"); + this.svg.append("g").attr("class", "stack-graph-highlights"); + this.svg.append("g").attr("class", "stack-graph-body"); + let labels = this.svg.append("g").attr("class", "stack-graph-labels"); + labels.append("rect").attr("class", "stack-graph-label-bg").attr("fill", "#ffffff"); + labels.append("text").attr("text-anchor", "middle") + .attr("transform", "rotate(270)") + .text("Call Stack"); + let axes = this.svg.append("g").attr("class", "stack-graph-axis"); + axes.append("rect").attr("class", "stack-graph-axis-bg").attr("fill", "#ffffff"); + this.svg.append("g").attr("class", "stack-graph-breaks"); + + let this_ = this; + let hasClass = (elt, id) => { + if (elt.classList) return elt.classList.contains(id); + else if (elt.className.baseVal) return elt.className.baseVal.indexOf(id) > -1; + else return elt.className.indexOf(id) > -1; + } + let mouseOpCallback = function(mouseover) { + return function(d, i, x) { + let elt = this; + if (this_._hoverHandler) { + if (hasClass(elt, "stack-graph-highlight") || hasClass(elt, "stack-graph-break")) { + if (mouseover) tooltip.show((d).summary, elt, "top"); + else tooltip.hide(); + } else if (hasClass(elt, "stack-graph-node")) { + this_._hoverHandler((d).data, mouseover); + } + } + }; + }; + this.mouseoverCallback = mouseOpCallback(true); + this.mouseoutCallback = mouseOpCallback(false); + } + + render(anim: boolean = false) { + // set up SVG + if (!this.svg) { + this.setupSvg(); + } + this.svg.attr('width', this._width) + .attr('height', this._height); + + let data = this.root ? this.root.descendants() : []; + if (this.zoomed) { + let curr = this.root.data, route = []; + while (curr = curr.parent) route.push(curr.name); + unzoomData.name = " " + route.join(" ← "); + let unzoomNode = new StackGraphNode(unzoomData); + unzoomNode.start = this.root.start; + unzoomNode.finish = this.root.finish; + unzoomNode.depth = -1; + data.splice(0, 0, unzoomNode); + } + + + /////////////////////////////////////////////////////////////////// + // update y label position + + let gLabel = this.svg.select("g.stack-graph-labels"); + gLabel.select("rect").attr("x", 0) + .attr("y", 0) + .attr("width", PADDING_X_LEFT) + .attr("height", this._height); + gLabel.select("text").attr("dy", PADDING_X_LEFT/2) + .attr("dx", -this._height/2); + + + /////////////////////////////////////////////////////////////////// + // create axes + + let xBase = d3.scaleLinear().range([PADDING_X_LEFT, this._width - PADDING_X_RIGHT]); + if (this.root) { + xBase.domain([this.root.start, this.root.finish]); + } + let [xMin, xMax] = xBase.domain(); + let xBaseTo01 = d3.scaleLinear().domain([xMin, xMax]); + let x = fc.scaleDiscontinuous(xBase); + let dcRange = fc.discontinuityRange(...this._discontinuities); + x.discontinuityProvider(dcRange); + let xTo01 = fc.scaleDiscontinuous(xBaseTo01); + xTo01.discontinuityProvider(dcRange); + + let ticks = d3.ticks(0, 1, X_AXIS_TICKS); + let tickValues = ticks.map(xTo01.invert); + + let y = d3.scaleLinear().range([0, this._cellHeight]); + + let xAxis = d3.axisBottom(x).tickValues(tickValues); + xAxis.tickFormat((x) => (x/1000).toFixed(3) + "s"); + + let xAxisPos = this._height - PADDING_Y_BOTTOM; + this.svg.select("g.stack-graph-axis") + .attr("transform", `translate(0,${xAxisPos})`) + .call(xAxis); + this.svg.select("rect.stack-graph-axis-bg") + .attr("x", 0).attr("y", 0).attr("width", this._width).attr("height", PADDING_Y_BOTTOM); + + let width = (d: StackGraphNode | StackGraphHighlight) => x(d.finish) - x(d.start); + + /////////////////////////////////////////////////////////////////// + // render highlights + + let gHigh = this.svg.select("g.stack-graph-highlights"); + let hRect = gHigh.selectAll("rect").data(this._highlights.filter((d) => width(d) > 0)); + let hEnter = hRect.enter().append("svg:rect").attr("class", "stack-graph-highlight"); + hEnter.on("mouseover", this.mouseoverCallback) + .on("mouseout", this.mouseoutCallback); + hRect.merge(hEnter) + .attr("height", this._height) + .attr("fill", (d: StackGraphHighlight) => d.color) + .attr("title", (d: StackGraphHighlight) => d.summary); + + // animate the highlights if requested + let hPos = (grp) => grp.attr("width", width) + .attr("transform", (d) => "translate(" + x(d.start) + ",0)"); + if (anim) { + hPos(hRect.transition() + .duration(this._transitionDuration).ease(this._transitionEase)); + } else { + hPos(hRect); + } + hPos(hEnter); + hRect.exit().remove(); + + /////////////////////////////////////////////////////////////////// + // render breaks + + let gBreak = this.svg.select("g.stack-graph-breaks"); + let bLine = gBreak.selectAll("line").data(this._highlights.filter((d) => width(d) == 0)); + let bEnter = bLine.enter().append("svg:line").attr("class", "stack-graph-break"); + bEnter.on("mouseover", this.mouseoverCallback) + .on("mouseout", this.mouseoutCallback); + bLine.merge(bEnter) + .attr("y1", this._height - PADDING_Y_BOTTOM + xAxis.tickSizeInner()) + .attr("y2", 0) + .attr("x1", 0) + .attr("x2", 0) + .attr("stroke-width", 1) + .attr("stroke", "#444444") + .style("stroke-dasharray", "5 5"); + + // animate the breaks if requested + let bPos = (grp) => grp.attr("transform", (d) => "translate(" + x(d.start) + ",0)"); + if (anim) { + bPos(bLine.transition() + .duration(this._transitionDuration).ease(this._transitionEase)); + } else { + bPos(bLine); + } + bPos(bEnter); + bLine.exit().remove(); + + + /////////////////////////////////////////////////////////////////// + // render call stacks + + let dx = (d: StackGraphNode) => x(d.start); + let dy = (d: StackGraphNode) => this._height - y(d.depth + (this.zoomed ? 1 : 0)) - this._cellHeight - PADDING_Y_BOTTOM; + let pos = (d: StackGraphNode) => `translate(${dx(d)},${dy(d)})`; + let rectClass = (d: StackGraphNode) => d.data === unzoomData ? "stack-graph-rect stack-graph-unzoom" : "stack-graph-rect"; + + let filteredData = data.filter((d) => width(d) >= MIN_WIDTH); + let g = this.svg.select("g.stack-graph-body") + .selectAll("g.stack-graph-node").data(filteredData, (d) => d.data.id); + + let enter = g.enter().append("svg:g"); + enter.append("svg:rect"); + enter.append("foreignObject") + .append("xhtml:div"); + enter.on("click", (d: StackGraphNode) => this.clickCallback(d)) + .on("mouseover", this.mouseoverCallback) + .on("mouseout", this.mouseoutCallback); + + g.merge(enter) + .attr("height", this._cellHeight) + .attr("name", (d: StackGraphNode) => d.data.name) + .attr("class", "stack-graph-node") + .select("rect") + .attr("height", this._cellHeight) + .attr("class", rectClass) + .attr("fill", (d: StackGraphNode) => this._color(d.data)); + + g.merge(enter) + .select("foreignObject") + .attr("width", width) + .attr("height", this._cellHeight) + .select("div") + .attr("class", "stack-graph-label") + .style("display", (d: StackGraphNode) => width(d) < 35 ? "none" : "block") + .style("color", (d: StackGraphNode) => this._textColor(d.data)) + .text((d: StackGraphNode) => d.data.name); + + // animate preexisting nodes into new positions + let gPos = (grp) => grp.attr("transform", pos) + .attr("width", width) + .select("rect") + .attr("width", width); + if (anim) { + gPos(g.transition() + .duration(this._transitionDuration).ease(this._transitionEase)); + } else { + gPos(g); + } + + // don't animate new nodes into position... + gPos(enter); + // but maybe fade them in if this isn't the first render (i.e., if unzooming) + if (anim) { + enter.style("opacity", 0.0) + .transition() + .duration(this._transitionDuration).ease(this._transitionEase) + .style("opacity", 1.0); + } + + g.exit().remove(); + } + + constructor(s: string) { + this.selector = s; + } + + } + + export function stackGraph(selector: string): StackGraph { + return new StackGraph(selector); + } +} \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/d3fc-discontinuous-scale.js b/rosette/lib/profile/renderer/report/html/js/d3fc-discontinuous-scale.js new file mode 100644 index 00000000..229b50f6 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/d3fc-discontinuous-scale.js @@ -0,0 +1,645 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (factory((global.fc = global.fc || {}))); + }(this, (function (exports) { 'use strict'; + + var createReboundMethod = (function (target, source, name) { + var method = source[name]; + if (typeof method !== 'function') { + throw new Error('Attempt to rebind ' + name + ' which isn\'t a function on the source object'); + } + return function () { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var value = method.apply(source, args); + return value === source ? target : value; + }; + }); + + var rebind = (function (target, source) { + for (var _len = arguments.length, names = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { + names[_key - 2] = arguments[_key]; + } + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = names[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var name = _step.value; + + target[name] = createReboundMethod(target, source, name); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return target; + }); + + var createTransform = function createTransform(transforms) { + return function (name) { + return transforms.reduce(function (name, fn) { + return name && fn(name); + }, name); + }; + }; + + var rebindAll = (function (target, source) { + for (var _len = arguments.length, transforms = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { + transforms[_key - 2] = arguments[_key]; + } + + var transform = createTransform(transforms); + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = Object.keys(source)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var name = _step.value; + + var result = transform(name); + if (result) { + target[result] = createReboundMethod(target, source, name); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return target; + }); + + var regexify = (function (strsOrRegexes) { + return strsOrRegexes.map(function (strOrRegex) { + return typeof strOrRegex === 'string' ? new RegExp('^' + strOrRegex + '$') : strOrRegex; + }); + }); + + var exclude = (function () { + for (var _len = arguments.length, exclusions = Array(_len), _key = 0; _key < _len; _key++) { + exclusions[_key] = arguments[_key]; + } + + exclusions = regexify(exclusions); + return function (name) { + return exclusions.every(function (exclusion) { + return !exclusion.test(name); + }) && name; + }; + }); + + var include = (function () { + for (var _len = arguments.length, inclusions = Array(_len), _key = 0; _key < _len; _key++) { + inclusions[_key] = arguments[_key]; + } + + inclusions = regexify(inclusions); + return function (name) { + return inclusions.some(function (inclusion) { + return inclusion.test(name); + }) && name; + }; + }); + + var includeMap = (function (mappings) { + return function (name) { + return mappings[name]; + }; + }); + + var capitalizeFirstLetter = function capitalizeFirstLetter(str) { + return str[0].toUpperCase() + str.slice(1); + }; + + var prefix = (function (prefix) { + return function (name) { + return prefix + capitalizeFirstLetter(name); + }; + }); + + exports.rebind = rebind; + exports.rebindAll = rebindAll; + exports.exclude = exclude; + exports.include = include; + exports.includeMap = includeMap; + exports.prefix = prefix; + + Object.defineProperty(exports, '__esModule', { value: true }); + + }))); + + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-scale'), require('d3fc-rebind'), require('d3-time')) : + typeof define === 'function' && define.amd ? define(['exports', 'd3-scale', 'd3fc-rebind', 'd3-time'], factory) : + (factory((global.fc = global.fc || {}),global.d3,global.fc,global.d3)); +}(this, (function (exports,d3Scale,d3fcRebind,d3Time) { 'use strict'; + +var identity = function () { + + var identity = {}; + + identity.distance = function (start, end) { + return end - start; + }; + + identity.offset = function (start, offset) { + return start instanceof Date ? new Date(start.getTime() + offset) : start + offset; + }; + + identity.clampUp = function (d) { + return d; + }; + + identity.clampDown = function (d) { + return d; + }; + + identity.copy = function () { + return identity; + }; + + return identity; +}; + +function tickFilter(ticks, discontinuityProvider) { + var discontinuousTicks = []; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = ticks[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var tick = _step.value; + + var up = discontinuityProvider.clampUp(tick); + var down = discontinuityProvider.clampDown(tick); + if (up === down) { + discontinuousTicks.push(up); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return discontinuousTicks; +} + +function discontinuous(adaptedScale) { + var _this = this; + + if (!arguments.length) { + adaptedScale = d3Scale.scaleIdentity(); + } + + var discontinuityProvider = identity(); + + var scale = function scale(value) { + var domain = adaptedScale.domain(); + var range = adaptedScale.range(); + + // The discontinuityProvider is responsible for determine the distance between two points + // along a scale that has discontinuities (i.e. sections that have been removed). + // the scale for the given point 'x' is calculated as the ratio of the discontinuous distance + // over the domain of this axis, versus the discontinuous distance to 'x' + var totalDomainDistance = discontinuityProvider.distance(domain[0], domain[1]); + var distanceToX = discontinuityProvider.distance(domain[0], value); + var ratioToX = distanceToX / totalDomainDistance; + var scaledByRange = ratioToX * (range[1] - range[0]) + range[0]; + return scaledByRange; + }; + + scale.invert = function (x) { + var domain = adaptedScale.domain(); + var range = adaptedScale.range(); + + var ratioToX = (x - range[0]) / (range[1] - range[0]); + var totalDomainDistance = discontinuityProvider.distance(domain[0], domain[1]); + var distanceToX = ratioToX * totalDomainDistance; + return discontinuityProvider.offset(domain[0], distanceToX); + }; + + scale.domain = function () { + if (!arguments.length) { + return adaptedScale.domain(); + } + var newDomain = arguments.length <= 0 ? undefined : arguments[0]; + + // clamp the upper and lower domain values to ensure they + // do not fall within a discontinuity + var domainLower = discontinuityProvider.clampUp(newDomain[0]); + var domainUpper = discontinuityProvider.clampDown(newDomain[1]); + adaptedScale.domain([domainLower, domainUpper]); + return scale; + }; + + scale.nice = function () { + adaptedScale.nice(); + var domain = adaptedScale.domain(); + var domainLower = discontinuityProvider.clampUp(domain[0]); + var domainUpper = discontinuityProvider.clampDown(domain[1]); + adaptedScale.domain([domainLower, domainUpper]); + return scale; + }; + + scale.ticks = function () { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var ticks = adaptedScale.ticks.apply(_this, args); + return tickFilter(ticks, discontinuityProvider); + }; + + scale.copy = function () { + return discontinuous(adaptedScale.copy()).discontinuityProvider(discontinuityProvider.copy()); + }; + + scale.discontinuityProvider = function () { + if (!arguments.length) { + return discontinuityProvider; + } + discontinuityProvider = arguments.length <= 0 ? undefined : arguments[0]; + return scale; + }; + + d3fcRebind.rebindAll(scale, adaptedScale, d3fcRebind.include('range', 'rangeRound', 'interpolate', 'clamp', 'tickFormat')); + + return scale; +} + +var skipWeekends = function () { + + // the indices returned by date.getDay() + var day = { + sunday: 0, + monday: 1, + saturday: 6 + }; + + var millisPerDay = 24 * 3600 * 1000; + var millisPerWorkWeek = millisPerDay * 5; + var millisPerWeek = millisPerDay * 7; + + var skipWeekends = {}; + + var isWeekend = function isWeekend(date) { + return date.getDay() === 0 || date.getDay() === 6; + }; + + skipWeekends.clampDown = function (date) { + if (date && isWeekend(date)) { + // round the date up to midnight + var newDate = d3Time.timeDay.ceil(date); + // then subtract the required number of days + if (newDate.getDay() === day.sunday) { + return d3Time.timeDay.offset(newDate, -1); + } else if (newDate.getDay() === day.monday) { + return d3Time.timeDay.offset(newDate, -2); + } else { + return newDate; + } + } else { + return date; + } + }; + + skipWeekends.clampUp = function (date) { + if (date && isWeekend(date)) { + // round the date down to midnight + var newDate = d3Time.timeDay.floor(date); + // then add the required number of days + if (newDate.getDay() === day.saturday) { + return d3Time.timeDay.offset(newDate, 2); + } else if (newDate.getDay() === day.sunday) { + return d3Time.timeDay.offset(newDate, 1); + } else { + return newDate; + } + } else { + return date; + } + }; + + // returns the number of included milliseconds (i.e. those which do not fall) + // within discontinuities, along this scale + skipWeekends.distance = function (startDate, endDate) { + startDate = skipWeekends.clampUp(startDate); + endDate = skipWeekends.clampDown(endDate); + + // move the start date to the end of week boundary + var offsetStart = d3Time.timeSaturday.ceil(startDate); + if (endDate < offsetStart) { + return endDate.getTime() - startDate.getTime(); + } + + var msAdded = offsetStart.getTime() - startDate.getTime(); + + // move the end date to the end of week boundary + var offsetEnd = d3Time.timeSaturday.ceil(endDate); + var msRemoved = offsetEnd.getTime() - endDate.getTime(); + + // determine how many weeks there are between these two dates + // round to account for DST transitions + var weeks = Math.round((offsetEnd.getTime() - offsetStart.getTime()) / millisPerWeek); + + return weeks * millisPerWorkWeek + msAdded - msRemoved; + }; + + skipWeekends.offset = function (startDate, ms) { + var date = isWeekend(startDate) ? skipWeekends.clampUp(startDate) : startDate; + + if (ms === 0) { + return date; + } + + var isNegativeOffset = ms < 0; + var isPositiveOffset = ms > 0; + var remainingms = ms; + + // move to the end of week boundary for a postive offset or to the start of a week for a negative offset + var weekBoundary = isNegativeOffset ? d3Time.timeMonday.floor(date) : d3Time.timeSaturday.ceil(date); + remainingms -= weekBoundary.getTime() - date.getTime(); + + // if the distance to the boundary is greater than the number of ms + // simply add the ms to the current date + if (isNegativeOffset && remainingms > 0 || isPositiveOffset && remainingms < 0) { + return new Date(date.getTime() + ms); + } + + // skip the weekend for a positive offset + date = isNegativeOffset ? weekBoundary : d3Time.timeDay.offset(weekBoundary, 2); + + // add all of the complete weeks to the date + var completeWeeks = Math.floor(remainingms / millisPerWorkWeek); + date = d3Time.timeDay.offset(date, completeWeeks * 7); + remainingms -= completeWeeks * millisPerWorkWeek; + + // add the remaining time + date = new Date(date.getTime() + remainingms); + return date; + }; + + skipWeekends.copy = function () { + return skipWeekends; + }; + + return skipWeekends; +}; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; +} : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; +}; + + + + + + + + + + + + + + + + + + + + + +var get = function get(object, property, receiver) { + if (object === null) object = Function.prototype; + var desc = Object.getOwnPropertyDescriptor(object, property); + + if (desc === undefined) { + var parent = Object.getPrototypeOf(object); + + if (parent === null) { + return undefined; + } else { + return get(parent, property, receiver); + } + } else if ("value" in desc) { + return desc.value; + } else { + var getter = desc.get; + + if (getter === undefined) { + return undefined; + } + + return getter.call(receiver); + } +}; + + + + + + + + + + + + + + + + + +var set = function set(object, property, value, receiver) { + var desc = Object.getOwnPropertyDescriptor(object, property); + + if (desc === undefined) { + var parent = Object.getPrototypeOf(object); + + if (parent !== null) { + set(parent, property, value, receiver); + } + } else if ("value" in desc && desc.writable) { + desc.value = value; + } else { + var setter = desc.set; + + if (setter !== undefined) { + setter.call(receiver, value); + } + } + + return value; +}; + +var provider = function provider() { + for (var _len = arguments.length, ranges = Array(_len), _key = 0; _key < _len; _key++) { + ranges[_key] = arguments[_key]; + } + + var inRange = function inRange(number, range) { + return number > range[0] && number < range[1]; + }; + + var surroundsRange = function surroundsRange(inner, outer) { + return inner[0] >= outer[0] && inner[1] <= outer[1]; + }; + + var identity = {}; + + identity.distance = function (start, end) { + start = identity.clampUp(start); + end = identity.clampDown(end); + + var surroundedRanges = ranges.filter(function (r) { + return surroundsRange(r, [start, end]); + }); + var rangeSizes = surroundedRanges.map(function (r) { + return r[1] - r[0]; + }); + + return end - start - rangeSizes.reduce(function (total, current) { + return total + current; + }, 0); + }; + + var add = function add(value, offset) { + return value instanceof Date ? new Date(value.getTime() + offset) : value + offset; + }; + + identity.offset = function (location, offset) { + if (offset > 0) { + var _ret = function () { + var currentLocation = identity.clampUp(location); + var offsetRemaining = offset; + while (offsetRemaining > 0) { + var futureRanges = ranges.filter(function (r) { + return r[0] > currentLocation; + }).sort(function (a, b) { + return a[0] - b[0]; + }); + if (futureRanges.length) { + var nextRange = futureRanges[0]; + var delta = nextRange[0] - currentLocation; + if (delta > offsetRemaining) { + currentLocation = add(currentLocation, offsetRemaining); + offsetRemaining = 0; + } else { + currentLocation = nextRange[1]; + offsetRemaining -= delta; + } + } else { + currentLocation = add(currentLocation, offsetRemaining); + offsetRemaining = 0; + } + } + return { + v: currentLocation + }; + }(); + + if ((typeof _ret === "undefined" ? "undefined" : _typeof(_ret)) === "object") return _ret.v; + } else { + var _ret2 = function () { + var currentLocation = identity.clampDown(location); + var offsetRemaining = offset; + while (offsetRemaining < 0) { + var futureRanges = ranges.filter(function (r) { + return r[1] < currentLocation; + }).sort(function (a, b) { + return b[0] - a[0]; + }); + if (futureRanges.length) { + var nextRange = futureRanges[0]; + var delta = nextRange[1] - currentLocation; + if (delta < offsetRemaining) { + currentLocation = add(currentLocation, offsetRemaining); + offsetRemaining = 0; + } else { + currentLocation = nextRange[0]; + offsetRemaining -= delta; + } + } else { + currentLocation = add(currentLocation, offsetRemaining); + offsetRemaining = 0; + } + } + return { + v: currentLocation + }; + }(); + + if ((typeof _ret2 === "undefined" ? "undefined" : _typeof(_ret2)) === "object") return _ret2.v; + } + }; + + identity.clampUp = function (d) { + return ranges.reduce(function (value, range) { + return inRange(value, range) ? range[1] : value; + }, d); + }; + + identity.clampDown = function (d) { + return ranges.reduce(function (value, range) { + return inRange(value, range) ? range[0] : value; + }, d); + }; + + identity.copy = function () { + return identity; + }; + + return identity; +}; + +exports.scaleDiscontinuous = discontinuous; +exports.discontinuitySkipWeekends = skipWeekends; +exports.discontinuityIdentity = identity; +exports.discontinuityRange = provider; + +Object.defineProperty(exports, '__esModule', { value: true }); + +}))); diff --git a/rosette/lib/profile/renderer/report/html/js/data.js b/rosette/lib/profile/renderer/report/html/js/data.js new file mode 100644 index 00000000..6e0373f9 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/data.js @@ -0,0 +1,252 @@ +var data; +(function (data_1) { + var SolverCallType; + (function (SolverCallType) { + SolverCallType[SolverCallType["SOLVE"] = 0] = "SOLVE"; + SolverCallType[SolverCallType["ENCODE"] = 1] = "ENCODE"; + SolverCallType[SolverCallType["FINITIZE"] = 2] = "FINITIZE"; + })(SolverCallType = data_1.SolverCallType || (data_1.SolverCallType = {})); + ; + // buffer messages until document ready + var bufferedMessages = []; + var ready = false; + var currentState = { + metadata: null, + root: null, + current: null, + idToNode: {}, + zeroMetrics: null, + solverCalls: [], + streaming: false + }; + var updateCallbacks = []; + function registerUpdateCallback(cb) { + updateCallbacks.push(cb); + } + data_1.registerUpdateCallback = registerUpdateCallback; + function unregisterUpdateCallback(cb) { + var i = updateCallbacks.indexOf(cb); + if (i > -1) + updateCallbacks.splice(i, 1); + } + data_1.unregisterUpdateCallback = unregisterUpdateCallback; + function readyForData() { + if (!ready) { + ready = true; + receiveData(bufferedMessages); + bufferedMessages = []; + } + } + data_1.readyForData = readyForData; + function diffMetrics(p1, p2) { + var ret = {}; + for (var k in p1) { + if (p2.hasOwnProperty(k)) { + ret[k] = p2[k] - p1[k]; + } + } + return ret; + } + function exclMetrics(incl, children) { + var ret = {}; + for (var k in incl) { + ret[k] = incl[k]; + } + for (var _i = 0, children_1 = children; _i < children_1.length; _i++) { + var c = children_1[_i]; + for (var k in incl) { + ret[k] -= c.incl[k] || 0; + } + } + return ret; + } + var messages; + (function (messages) { + function receiveMetadataMessage(msg) { + delete msg["type"]; + currentState.metadata = msg; + } + messages.receiveMetadataMessage = receiveMetadataMessage; + // update the ProfileState using data from the profile message + function receiveCallgraphMessage(msg) { + var evts = msg["events"]; + if (evts.length == 0) + return; + if (!currentState.zeroMetrics) { + currentState.zeroMetrics = evts[0].metrics; + } + for (var _i = 0, evts_1 = evts; _i < evts_1.length; _i++) { + var e = evts_1[_i]; + if (e.type == "ENTER") { + if (!currentState.current && currentState.root) { + console.error("multiple root procedure calls"); + return; + } + // e has fields: + // id, function, location, inputs, metrics + var dm = diffMetrics(currentState.zeroMetrics, e.metrics); + var node = { + id: e.id, + name: e.function, + callsite: e.callsite, + source: e.source, + start: dm.time, + finish: null, + startMetrics: dm, + finishMetrics: null, + isFinished: false, + inputs: e.inputs, + outputs: {}, + children: [], + parent: currentState.current, + incl: {}, + excl: {}, + score: 0 + }; + if (currentState.current) + currentState.current.children.push(node); + if (!currentState.root) // might be the first call + currentState.root = node; + currentState.current = node; + currentState.idToNode[e.id] = node; + } + else if (e.type == "EXIT") { + if (!currentState.current) { + console.error("unbalanced EXIT event"); + } + // e has fields: + // outputs, metrics + var dm = diffMetrics(currentState.zeroMetrics, e.metrics); + currentState.current.finish = dm.time; + currentState.current.finishMetrics = dm; + currentState.current.outputs = e.outputs; + currentState.current.isFinished = true; + currentState.current.incl = diffMetrics(currentState.current.startMetrics, dm); + currentState.current.excl = exclMetrics(currentState.current.incl, currentState.current.children); + currentState.current = currentState.current.parent; + } + } + // set fake finish times and metrics for unclosed nodes + var fakeFinishMetrics = diffMetrics(currentState.zeroMetrics, evts[evts.length - 1].metrics); + var fakeFinishTime = fakeFinishMetrics.time; + var curr = currentState.current; + while (curr) { + if (!curr.isFinished) { + curr.finish = fakeFinishTime; + curr.finishMetrics = fakeFinishMetrics; + curr.incl = diffMetrics(curr.startMetrics, fakeFinishMetrics); + curr.excl = exclMetrics(curr.incl, curr.children); + } + curr = curr.parent; + } + } + messages.receiveCallgraphMessage = receiveCallgraphMessage; + function receiveSolverCallsMessage(msg) { + var events = msg["events"]; + if (!currentState.zeroMetrics) { + console.error("solver-calls expects profile data first"); + return; + } + var startTime = currentState.zeroMetrics.time; + for (var _i = 0, events_1 = events; _i < events_1.length; _i++) { + var e = events_1[_i]; + if (e.type == "start") { + var typ = e.part == "solver" ? SolverCallType.SOLVE : (e.part == "encode" ? SolverCallType.ENCODE : SolverCallType.FINITIZE); + currentState.solverCalls.push({ + type: typ, + start: e.time - startTime, + finish: undefined, + sat: undefined + }); + } + else if (e.type == "finish") { + if (currentState.solverCalls.length > 0) { + var curr = currentState.solverCalls[currentState.solverCalls.length - 1]; + curr.finish = e.time - startTime; + if (curr.type == SolverCallType.SOLVE) + curr.sat = e.sat; + } + } + } + } + messages.receiveSolverCallsMessage = receiveSolverCallsMessage; + function receiveUnusedTermsMessage(msg) { + var data = msg["data"]; // list of (call-id, #unused) pairs + for (var _i = 0, data_2 = data; _i < data_2.length; _i++) { + var pair = data_2[_i]; + var id = pair[0].toString(), num = pair[1]; + if (currentState.idToNode.hasOwnProperty(id)) { + var node = currentState.idToNode[id]; + node.excl["unused-terms"] = num; + } + } + } + messages.receiveUnusedTermsMessage = receiveUnusedTermsMessage; + })(messages || (messages = {})); + var stream; + (function (stream) { + var webSocket; + function receiveStreamMessage(msg) { + if (msg["event"] == "start") { + currentState.streaming = true; + webSocket = new WebSocket(msg["url"]); + webSocket.onmessage = webSocketMessageCallback; + webSocket.onerror = webSocketErrorCallback; + } + else if (msg["event"] == "finish") { + currentState.streaming = false; + if (webSocket) + webSocket.close(); + } + else { + console.log("unknown stream message:", msg); + } + } + stream.receiveStreamMessage = receiveStreamMessage; + function webSocketMessageCallback(evt) { + var msgs = JSON.parse(evt.data); // will be a list of messages + receiveData(msgs); + } + function webSocketErrorCallback(evt) { + alert("Could not open the WebSocket connection for streaming. This might happen if the profiler is not currently running."); + } + })(stream || (stream = {})); + // hand messages to their handler functions + function receiveMessages(msgs) { + for (var _i = 0, msgs_1 = msgs; _i < msgs_1.length; _i++) { + var msg = msgs_1[_i]; + if (msg.type == "metadata") { + messages.receiveMetadataMessage(msg); + } + else if (msg.type == "callgraph") { + messages.receiveCallgraphMessage(msg); + } + else if (msg.type == "solver-calls") { + messages.receiveSolverCallsMessage(msg); + } + else if (msg.type == "unused-terms") { + messages.receiveUnusedTermsMessage(msg); + } + else if (msg.type === "stream") { + stream.receiveStreamMessage(msg); + } + else { + console.log("unknown message:", msg); + } + } + } + function receiveData(msgs) { + if (ready) { + receiveMessages(msgs); + for (var _i = 0, updateCallbacks_1 = updateCallbacks; _i < updateCallbacks_1.length; _i++) { + var cb = updateCallbacks_1[_i]; + cb(currentState); + } + } + else { + bufferedMessages.push.apply(bufferedMessages, msgs); + } + } + data_1.receiveData = receiveData; +})(data || (data = {})); +//# sourceMappingURL=data.js.map \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/data.ts b/rosette/lib/profile/renderer/report/html/js/data.ts new file mode 100644 index 00000000..7c8345bb --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/data.ts @@ -0,0 +1,282 @@ +namespace data { + export interface CallNode { + id: string; + name: string; + callsite: string; + source: string; + start: number; // time + finish: number; // time + startMetrics: Map; + finishMetrics: Map; + isFinished: boolean; // have we seen an exit event for this node? + inputs: Map; + outputs: Map; + children: CallNode[]; + parent: CallNode; + incl: Map; + excl: Map; + score: number; + } + + export interface ProfileState { + metadata: Object; + root: CallNode; + current: CallNode; + idToNode: Map; + zeroMetrics: Map; + solverCalls: SolverCall[]; + streaming: boolean; + } + + export interface SolverCall { + type: SolverCallType, + start: number; + finish: number; + sat: boolean; + } + + export enum SolverCallType { + SOLVE, + ENCODE, + FINITIZE + }; + + // events are also a type of message, since they have a type field + interface Message { + type: string + } + + // buffer messages until document ready + var bufferedMessages: Message[] = []; + var ready = false; + var currentState: ProfileState = { + metadata: null, + root: null, + current: null, + idToNode: {}, + zeroMetrics: null, + solverCalls: [], + streaming: false + }; + + // callbacks to invoke when new state data arrives + + type StateUpdateCallback = (msg: ProfileState) => void; + var updateCallbacks: StateUpdateCallback[] = []; + + export function registerUpdateCallback(cb: StateUpdateCallback): void { + updateCallbacks.push(cb); + } + export function unregisterUpdateCallback(cb: StateUpdateCallback): void { + let i = updateCallbacks.indexOf(cb); + if (i > -1) + updateCallbacks.splice(i, 1); + } + + export function readyForData() { + if (!ready) { + ready = true; + receiveData(bufferedMessages); + bufferedMessages = []; + } + } + + function diffMetrics(p1: Map, p2: Map): Map { + let ret: Map = {}; + for (let k in p1) { + if (p2.hasOwnProperty(k)) { + ret[k] = p2[k] - p1[k]; + } + } + return ret; + } + + function exclMetrics(incl: Map, children: CallNode[]): Map { + let ret = {}; + for (let k in incl) { + ret[k] = incl[k]; + } + for (let c of children) { + for (let k in incl) { + ret[k] -= c.incl[k] || 0; + } + } + return ret; + } + + namespace messages { + export function receiveMetadataMessage(msg: Message): void { + delete msg["type"]; + currentState.metadata = msg; + } + + // update the ProfileState using data from the profile message + export function receiveCallgraphMessage(msg: Message): void { + let evts = msg["events"]; + if (evts.length == 0) return; + + if (!currentState.zeroMetrics) { + currentState.zeroMetrics = evts[0].metrics; + } + + for (let e of evts) { + if (e.type == "ENTER") { + if (!currentState.current && currentState.root) { + console.error("multiple root procedure calls"); + return; + } + // e has fields: + // id, function, location, inputs, metrics + let dm = diffMetrics(currentState.zeroMetrics, e.metrics); + let node: CallNode = { + id: e.id, + name: e.function, + callsite: e.callsite, + source: e.source, + start: dm.time, + finish: null, + startMetrics: dm, + finishMetrics: null, + isFinished: false, + inputs: e.inputs, + outputs: {}, + children: [], + parent: currentState.current, + incl: {}, + excl: {}, + score: 0 + }; + if (currentState.current) + currentState.current.children.push(node); + if (!currentState.root) // might be the first call + currentState.root = node; + currentState.current = node; + currentState.idToNode[e.id] = node; + } else if (e.type == "EXIT") { + if (!currentState.current) { + console.error("unbalanced EXIT event"); + } + // e has fields: + // outputs, metrics + let dm = diffMetrics(currentState.zeroMetrics, e.metrics); + currentState.current.finish = dm.time; + currentState.current.finishMetrics = dm; + currentState.current.outputs = e.outputs; + currentState.current.isFinished = true; + currentState.current.incl = diffMetrics(currentState.current.startMetrics, dm); + currentState.current.excl = exclMetrics(currentState.current.incl, currentState.current.children); + currentState.current = currentState.current.parent; + } + } + + // set fake finish times and metrics for unclosed nodes + let fakeFinishMetrics = diffMetrics(currentState.zeroMetrics, evts[evts.length-1].metrics); + let fakeFinishTime = fakeFinishMetrics.time; + let curr = currentState.current; + while (curr) { + if (!curr.isFinished) { + curr.finish = fakeFinishTime; + curr.finishMetrics = fakeFinishMetrics; + curr.incl = diffMetrics(curr.startMetrics, fakeFinishMetrics); + curr.excl = exclMetrics(curr.incl, curr.children); + } + curr = curr.parent; + } + } + + export function receiveSolverCallsMessage(msg: Message): void { + let events = msg["events"]; + if (!currentState.zeroMetrics) { + console.error("solver-calls expects profile data first"); + return; + } + let startTime = currentState.zeroMetrics.time; + for (let e of events) { + if (e.type == "start") { + let typ = e.part == "solver" ? SolverCallType.SOLVE : (e.part == "encode" ? SolverCallType.ENCODE : SolverCallType.FINITIZE); + currentState.solverCalls.push({ + type: typ, + start: e.time - startTime, + finish: undefined, + sat: undefined + }); + } else if (e.type == "finish") { + if (currentState.solverCalls.length > 0) { + let curr = currentState.solverCalls[currentState.solverCalls.length - 1]; + curr.finish = e.time - startTime; + if (curr.type == SolverCallType.SOLVE) + curr.sat = e.sat; + } + } + } + } + + export function receiveUnusedTermsMessage(msg: Message): void { + let data: number[][] = msg["data"]; // list of (call-id, #unused) pairs + for (let pair of data) { + let id = pair[0].toString(), num = pair[1]; + if (currentState.idToNode.hasOwnProperty(id)) { + let node = currentState.idToNode[id]; + node.excl["unused-terms"] = num; + } + } + } + } + + namespace stream { + var webSocket: WebSocket; + + export function receiveStreamMessage(msg: Message): void { + if (msg["event"] == "start") { + currentState.streaming = true; + webSocket = new WebSocket(msg["url"]); + webSocket.onmessage = webSocketMessageCallback; + webSocket.onerror = webSocketErrorCallback; + } else if (msg["event"] == "finish") { + currentState.streaming = false; + if (webSocket) webSocket.close(); + } else { + console.log("unknown stream message:", msg); + } + } + + function webSocketMessageCallback(evt: MessageEvent): void { + let msgs = JSON.parse(evt.data); // will be a list of messages + receiveData(msgs); + } + + function webSocketErrorCallback(evt: Event) { + alert("Could not open the WebSocket connection for streaming. This might happen if the profiler is not currently running."); + } + } + + // hand messages to their handler functions + function receiveMessages(msgs: Message[]): void { + for (let msg of msgs) { + if (msg.type == "metadata") { + messages.receiveMetadataMessage(msg); + } else if (msg.type == "callgraph") { + messages.receiveCallgraphMessage(msg); + } else if (msg.type == "solver-calls") { + messages.receiveSolverCallsMessage(msg); + } else if (msg.type == "unused-terms") { + messages.receiveUnusedTermsMessage(msg); + } else if (msg.type === "stream") { + stream.receiveStreamMessage(msg); + } else { + console.log("unknown message:", msg); + } + } + } + + export function receiveData(msgs: Message[]): void { + if (ready) { + receiveMessages(msgs); + for (let cb of updateCallbacks) { + cb(currentState); + } + } else { + bufferedMessages.push(...msgs); + } + } +} diff --git a/rosette/lib/profile/renderer/report/html/js/profile.js b/rosette/lib/profile/renderer/report/html/js/profile.js new file mode 100644 index 00000000..eb6e982f --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/profile.js @@ -0,0 +1,497 @@ +var profile; +(function (profile) { + var calltable; + (function (calltable) { + var scoreColumns = [ + { type: "excl", column: "time", name: "Time (ms)", description: "Total time spent in this function (but not in descendent calls)", score: true }, + { type: "excl", column: "term-count", name: "Term Count", description: "Number of symbolic terms created", score: true }, + { type: "excl", column: "unused-terms", name: "Unused Terms", description: "Number of symbolic terms created that were never used for solving", score: true }, + { type: "excl", column: "union-size", name: "Union Size", description: "Total number of branches in all symbolic unions created", score: true }, + { type: "excl", column: "merge-cases", name: "Merge Cases", description: "Number of branches used during merging", score: true }, + ]; + var DOM_ROW_KEY = "symproRowObject"; + var PRUNE_SCORE_FACTOR = 0.01; // < 1% of max score = pruned + var colorScheme = makeScoreColorScheme(["#000000", "#FD893C", "#D9002C"]); + var tableSorter; + var contextDepth = 0; + var useSignatures = false; + var useCallsites = false; + var pruneSmallRows = true; + var idToRow = {}; + function initCallTable() { + renderTableHeaders(); + analysis.setAggregate(true); + analysis.setColumns(scoreColumns); + analysis.registerAnalysisCallback(receiveData); + } + calltable.initCallTable = initCallTable; + function renderTableHeaders() { + var thead = document.querySelector("#calltable thead"); + var tr = document.createElement("tr"); + makeCell("Function", tr, "th"); + var scoreCell = makeCell("Score", tr, "th"); + scoreCell.className = "sort-default score"; + scoreCell.id = "calltable-score-header"; + var keys = []; + for (var i = 0; i < scoreColumns.length; i++) { + var c = scoreColumns[i]; + var cell = makeCell(c.name, tr, "th"); + if (c.description) { + cell.dataset["title"] = c.description; + cell.addEventListener("mouseover", function (evt) { return tooltip.showWithDelay("", evt.target, "top", 100); }); + cell.addEventListener("mouseout", function (evt) { return tooltip.hide(); }); + } + var checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = true; + checkbox.className = "score-checkbox"; + checkbox.value = i.toString(); + checkbox.addEventListener("change", scoreSelectCallback); + checkbox.addEventListener("click", function (evt) { return evt.stopPropagation(); }); // prevent tablesorter from firing + cell.insertAdjacentElement("beforeend", checkbox); + } + thead.insertAdjacentElement("beforeend", tr); + // set up configuration controls + var config = document.getElementById("calltable-config"); + // aggregate checkbox + var agg = config.querySelector("#calltable-aggregate"); + agg.checked = true; + agg.addEventListener("change", configChangeCallback); + // context slider + var ctx = config.querySelector("#calltable-context"); + ctx.value = "0"; + var isIE = !!navigator.userAgent.match(/Trident/g) || !!navigator.userAgent.match(/MSIE/g); + ctx.addEventListener(isIE ? "change" : "input", configChangeCallback); + // context count + var ctxn = config.querySelector("#calltable-context-n"); + ctxn.textContent = "0"; + // filter checkbox + var fil = config.querySelector("#calltable-prune"); + fil.checked = true; + fil.addEventListener("change", configChangeCallback); + // collapse rosette checkbox + var clr = config.querySelector("#calltable-collapse-rosette"); + clr.checked = false; + clr.addEventListener("change", configChangeCallback); + // collapse solver checkbox + var cls = config.querySelector("#calltable-collapse-solver"); + cls.checked = false; + cls.addEventListener("change", configChangeCallback); + // signature checkbox + var sig = config.querySelector("#calltable-signature"); + sig.checked = false; + sig.addEventListener("change", configChangeCallback); + // callsites checkbox + var css = config.querySelector("#calltable-callsites"); + css.checked = false; + css.parentElement.style.display = "none"; + css.addEventListener("change", configChangeCallback); + // score checkbox + var sco = config.querySelector("#calltable-show-scoreboxes"); + sco.checked = false; + sco.addEventListener("change", configChangeCallback); + // more config + var more = config.querySelector("#calltable-config-more"); + more.style.display = "none"; + var moreLink = config.querySelector("#calltable-config-toggle-more"); + moreLink.addEventListener("click", toggleMoreCallback); + // attach event handler for table body + var tbody = document.querySelector("#calltable tbody"); + tbody.addEventListener("mouseover", hoverCallback); + tbody.addEventListener("mouseout", hoverCallback); + tableSorter = new Tablesort(document.getElementById("calltable"), { descending: true }); + } + function configChangeCallback(evt) { + var elt = this; + if (elt.id == "calltable-aggregate") { + var lbl = document.getElementById("calltable-callsites").parentElement; + lbl.style.display = (elt.checked ? "none" : ""); + analysis.setAggregate(elt.checked); + } + else if (elt.id == "calltable-context") { + contextDepth = elt.value == elt.max ? -1 : parseInt(elt.value); + var ctxn = document.getElementById("calltable-context-n"); + ctxn.textContent = contextDepth >= 0 ? elt.value : "∞"; + analysis.setContextDepth(contextDepth); + } + else if (elt.id == "calltable-prune") { + pruneSmallRows = elt.checked; + analysis.refresh(); + } + else if (elt.id == "calltable-collapse-rosette") { + analysis.setCollapseRosette(elt.checked); + } + else if (elt.id == "calltable-collapse-solver") { + stackgraph.setCollapseSolverTime(elt.checked); + analysis.setCollapseSolver(elt.checked); + } + else if (elt.id == "calltable-signature") { + useSignatures = elt.checked; + analysis.setSignatures(elt.checked); + } + else if (elt.id == "calltable-callsites") { + useCallsites = elt.checked; + analysis.refresh(); + } + else if (elt.id == "calltable-show-scoreboxes") { + var boxes = document.querySelectorAll("#calltable .score-checkbox"); + for (var i = 0; i < boxes.length; i++) { + boxes[i].style.display = elt.checked ? "initial" : "none"; + if (!elt.checked) { + scoreColumns[i].score = true; + } + } + if (!elt.checked) { + analysis.setColumns(scoreColumns); + } + } + windowResizeCallback(); + } + function toggleMoreCallback(evt) { + evt.preventDefault(); + var more = document.getElementById("calltable-config-more"); + var elt = this; + if (more.style.display == "none") { + more.style.display = "block"; + this.textContent = "[Less]"; + } + else { + more.style.display = "none"; + this.textContent = "[More]"; + } + } + function hoverCallback(evt) { + if (evt.type == "mouseover") { + var tgt = evt.target; + while (tgt && tgt.tagName != "TR") + tgt = tgt.parentElement; + stackgraph.calltableHoverCallback(tgt[DOM_ROW_KEY].allNodes); + } + else { + stackgraph.calltableHoverCallback([]); + } + } + function scoreSelectCallback(evt) { + var elt = this; + var idx = parseInt(elt.value); + scoreColumns[idx].score = elt.checked; + analysis.setColumns(scoreColumns); + } + function stackgraphHoverCallback(node, enter) { + var rows = document.querySelectorAll("#calltable tbody tr"); + for (var i = 0; i < rows.length; i++) { + var row = rows[i]; + row.className = ""; + } + if (enter) { + var hiRow = idToRow[node.id]; + if (hiRow) { + hiRow.className = "calltable-highlight"; + } + } + } + calltable.stackgraphHoverCallback = stackgraphHoverCallback; + function receiveData(state) { + renderTableRows(state.rows, state.aggregate, state.maxScore); + } + function renderPrettySource(source, cell, iscallsite) { + if (iscallsite === void 0) { iscallsite = false; } + if (source) { + var prettySource = getPrettySource(source); + if (iscallsite) + prettySource = "from " + prettySource; + var sourceSpan = makeCell(prettySource, cell, "span"); + sourceSpan.className = "source"; + sourceSpan.title = source; + } + } + function getPrettySource(source) { + var sourceParts = source.split(":"); + if (sourceParts.length != 3) + return source; + var file = sourceParts[0], line = sourceParts[1], col = sourceParts[2]; + var fileParts = file.split("/"); + return fileParts[fileParts.length - 1] + ":" + line; + } + function renderTableRows(rows, aggregate, maxScore) { + // remove old rows + var tbody = document.querySelector("#calltable tbody"); + while (tbody.firstChild) + tbody.removeChild(tbody.firstChild); + idToRow = {}; + // create new rows + for (var _i = 0, rows_1 = rows; _i < rows_1.length; _i++) { + var r = rows_1[_i]; + var row = document.createElement("tr"); + if (pruneSmallRows && r.score < maxScore * PRUNE_SCORE_FACTOR) { + continue; + } + // render the function name + var nameCell = makeCell(r.function, row); + nameCell.className = "name"; + if (useCallsites && !aggregate) { + renderPrettySource(r.node.callsite, nameCell, true); + } + else { + renderPrettySource(r.node.source, nameCell); + } + if (aggregate) { + var txt = r.allNodes.length > 1 ? formatNum(r.allNodes.length) + " calls" : "1 call"; + var numCallsSpan = makeCell(txt, nameCell, "span"); + numCallsSpan.className = "numcalls"; + } + // maybe render the signature + if (useSignatures) { + var inputs = r.node.inputs["signature"] || []; + var outputs = r.node.outputs["signature"] || []; + var istr = inputs.map(function (s) { return s[0].toUpperCase(); }).join(""); + var ostr = outputs.map(function (s) { return s[0].toUpperCase(); }).join(""); + var span = makeCell(istr + "→" + ostr, nameCell, "span"); + span.className = "signature"; + } + // render the call context if requested + if (contextDepth != 0) { + var contextList = document.createElement("ul"); + contextList.className = "context-list"; + var n = contextDepth, curr = r.node; + if (n == -1) { // if "infinite", collapse recursion + var count = 0; + while (curr = curr.parent) { + if (curr.parent && curr.parent.name == curr.name && curr.parent.source == curr.source) { + count += 1; + } + else { + var name_1 = curr.name + (count > 0 ? " ×" + (count + 1) : ""); + var li = makeCell(name_1, contextList, "li"); + count = 0; + renderPrettySource(curr.source, li); + } + } + } + else { + while (n-- != 0 && (curr = curr.parent)) { + var li = makeCell(curr.name, contextList, "li"); + renderPrettySource(curr.source, li); + } + } + nameCell.insertAdjacentElement("beforeend", contextList); + } + // score cell + var scoreBar = document.createElement("div"); + scoreBar.className = "scorebar"; + scoreBar.style.width = (r.score / scoreColumns.length * 100) + "%"; + scoreBar.style.backgroundColor = colorScheme(r.score); + var scoreBarCell = document.createElement("div"); + scoreBarCell.className = "scorecell-bar"; + scoreBarCell.insertAdjacentElement("beforeend", scoreBar); + var scoreSpan = document.createElement("span"); + scoreSpan.textContent = formatNum(r.score, 1, false, true); + scoreSpan.style.color = colorScheme(r.score); + var scoreSpanCell = document.createElement("div"); + scoreSpanCell.className = "scorecell-score"; + scoreSpanCell.insertAdjacentElement("beforeend", scoreSpan); + var scoreCell = makeCell("", row); + scoreCell.dataset["sort"] = r.score.toFixed(16); + scoreCell.insertAdjacentElement("beforeend", scoreSpanCell); + scoreCell.insertAdjacentElement("beforeend", scoreBarCell); + // data columns + for (var _a = 0, _b = r.columns; _a < _b.length; _a++) { + var k = _b[_a]; + makeCell(formatNum(k, 0), row); + } + // attach row object to the row + row[DOM_ROW_KEY] = r; + // record IDs + for (var _c = 0, _d = r.allNodes; _c < _d.length; _c++) { + var n = _d[_c]; + idToRow[n.id] = row; + } + tbody.insertAdjacentElement("beforeend", row); + } + // refresh the sort + tableSorter.refresh(); + } + function makeCell(str, row, type) { + if (type === void 0) { type = "td"; } + var elt = document.createElement(type); + elt.textContent = str; + row.insertAdjacentElement('beforeend', elt); + return elt; + } + })(calltable || (calltable = {})); + var stackgraph; + (function (stackgraph_1) { + var stackgraph; + var colorScheme = makeScoreColorScheme(["#ffeda0", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#bd0026"]); + var solverHighlightColor = "#E3F2FF"; + var lastWidth = 0; + var useDiscontinuities = false; + function initStackGraph() { + // build stackgraph + var STACK_WIDTH = document.getElementById("stackgraph").clientWidth; + var STACK_HEIGHT = 270; + lastWidth = STACK_WIDTH; + stackgraph = d3_stackgraph.stackGraph("#stackgraph"); + stackgraph.width(STACK_WIDTH).height(STACK_HEIGHT); + stackgraph.clickHandler(clickCallback); + stackgraph.hoverHandler(hoverCallback); + stackgraph.color(nodeColorCallback); + stackgraph.textColor(nodeTextColorCallback); + stackgraph.render(); + analysis.registerAnalysisCallback(receiveData); + analysis.registerZoomCallback(receiveZoom); + } + stackgraph_1.initStackGraph = initStackGraph; + function makeHighlightList(state) { + var ret = []; + for (var _i = 0, _a = state.solverCalls; _i < _a.length; _i++) { + var call = _a[_i]; + var finish = typeof call.finish === "undefined" ? state.root.finish : call.finish; + var dt = formatNum(finish - call.start, 1); + var summary = ""; + if (call.type == data.SolverCallType.SOLVE) { + var sat = typeof call.sat === "undefined" ? " (pending)" : (call.sat ? " (SAT)" : " (UNSAT)"); + summary = "Solver call" + sat + ": " + dt + "ms"; + } + else if (call.type == data.SolverCallType.ENCODE) { + summary = "Solver encoding: " + dt + "ms"; + } + else if (call.type == data.SolverCallType.FINITIZE) { + summary = "Solver finitization: " + dt + "ms"; + } + ret.push({ + start: call.start, + finish: finish, + color: solverHighlightColor, + summary: summary + }); + } + return ret; + } + function makeDiscontinuityList(state) { + var ret = []; + for (var _i = 0, _a = state.solverCalls; _i < _a.length; _i++) { + var call = _a[_i]; + var finish = typeof call.finish === "undefined" ? state.root.finish : call.finish; + ret.push([call.start, finish]); + } + return ret; + } + function receiveData(state) { + if (state.root) + stackgraph.data(state.root); + stackgraph.highlights(makeHighlightList(state)); + var discs = useDiscontinuities ? makeDiscontinuityList(state) : []; + stackgraph.discontinuities(discs); + stackgraph.render(); + } + function receiveZoom(node) { + stackgraph.zoom(node); + } + function clickCallback(node) { + analysis.zoomTo(node); + } + function windowResizeCallback() { + var width = document.getElementById("stackgraph").clientWidth; + if (width != lastWidth) { + stackgraph.width(width).render(); + lastWidth = width; + } + } + stackgraph_1.windowResizeCallback = windowResizeCallback; + function calltableHoverCallback(nodes) { + stackgraph.highlightData(nodes); + } + stackgraph_1.calltableHoverCallback = calltableHoverCallback; + function setCollapseSolverTime(use) { + useDiscontinuities = use; + } + stackgraph_1.setCollapseSolverTime = setCollapseSolverTime; + function hoverCallback(node, enter) { + calltable.stackgraphHoverCallback(node, enter); + } + function nodeColorCallback(node) { + return colorScheme(node.score); + } + function nodeTextColorCallback(node) { + var col = d3.color(colorScheme(node.score)).rgb(); + var yiq = ((col.r * 299) + (col.g * 587) + (col.b * 114)) / 1000; + return (yiq >= 128) ? "#222222" : "#eeeeee"; + } + })(stackgraph || (stackgraph = {})); + function formatNum(v, places, sig, always) { + if (places === void 0) { places = 6; } + if (sig === void 0) { sig = false; } + if (always === void 0) { always = false; } + var pl = sig ? (v < 10 ? 1 : 0) : places; + return (v % 1 == 0 && !always) ? v.toString() : v.toFixed(pl); + } + var metadataSet = false; + function setMetadata(state) { + if (!metadataSet && state.metadata != null) { + document.getElementById("profile-source").textContent = state.metadata["source"]; + document.getElementById("profile-time").textContent = state.metadata["time"]; + document.title = "Profile for " + state.metadata["source"] + " generated at " + state.metadata["time"]; + metadataSet = true; + } + } + var spinnerVisible = false; + function toggleStreamingSpinner(state) { + var elt = document.getElementById("progress"); + if (state.streaming && !spinnerVisible) { + elt.style.display = "block"; + spinnerVisible = true; + } + else if (!state.streaming && spinnerVisible) { + elt.style.display = "none"; + spinnerVisible = false; + } + } + var resizing = false; + function windowResizeCallback() { + if (!resizing) { + resizing = true; + stackgraph.windowResizeCallback(); + window.setTimeout(function () { + resizing = false; + }, 50); + } + } + // make a color scheme reflecting that scores > 4 are "very bad" + function makeScoreColorScheme(colors) { + var cs = d3.interpolateRgbBasis(colors); + return function (x) { + if (x > 4.0) + return colors[colors.length - 1]; + return cs(x / 4.0); + }; + } + function bindHelpEventHandlers() { + var helps = document.querySelectorAll(".help"); + for (var i = 0; i < helps.length; i++) { + var elt = helps[i]; + elt.addEventListener("mouseover", function (evt) { return tooltip.showWithDelay("", evt.target, "top", 100); }); + elt.addEventListener("mouseout", function (evt) { return tooltip.hide(); }); + } + } + function init() { + // set up tooltips + tooltip.init(); + bindHelpEventHandlers(); + // set up analysis + analysis.init(); + // initialize UI components + stackgraph.initStackGraph(); + calltable.initCallTable(); + // set up UI callbacks for data state changes + data.registerUpdateCallback(setMetadata); + data.registerUpdateCallback(toggleStreamingSpinner); + // receive all the data + data.readyForData(); + // set initial widths correctly now that data is rendered + windowResizeCallback(); + window.addEventListener("resize", windowResizeCallback); + } + document.addEventListener("DOMContentLoaded", init); +})(profile || (profile = {})); +//# sourceMappingURL=profile.js.map \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/profile.ts b/rosette/lib/profile/renderer/report/html/js/profile.ts new file mode 100644 index 00000000..7c674a9b --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/profile.ts @@ -0,0 +1,535 @@ +declare var d3; +declare var Tablesort; + +interface Map { [key: string]: T} + +namespace profile { + import CallNode = data.CallNode; + import ProfileState = data.ProfileState; + + namespace calltable { + const scoreColumns: analysis.AnalysisColumn[] = [ + {type: "excl", column: "time", name: "Time (ms)", description: "Total time spent in this function (but not in descendent calls)", score: true}, + {type: "excl", column: "term-count", name: "Term Count", description: "Number of symbolic terms created", score: true}, + {type: "excl", column: "unused-terms", name: "Unused Terms", description: "Number of symbolic terms created that were never used for solving", score: true}, + {type: "excl", column: "union-size", name: "Union Size", description: "Total number of branches in all symbolic unions created", score: true}, + {type: "excl", column: "merge-cases", name: "Merge Cases", description: "Number of branches used during merging", score: true}, + ]; + + const DOM_ROW_KEY = "symproRowObject"; + + const PRUNE_SCORE_FACTOR = 0.01; // < 1% of max score = pruned + + const colorScheme = makeScoreColorScheme(["#000000", "#FD893C", "#D9002C"]); + + var tableSorter; + var contextDepth = 0; + var useSignatures = false; + var useCallsites = false; + var pruneSmallRows = true; + var idToRow: Map = {}; + + export function initCallTable() { + renderTableHeaders(); + analysis.setAggregate(true); + analysis.setColumns(scoreColumns); + analysis.registerAnalysisCallback(receiveData); + } + + function renderTableHeaders(): void { + let thead = document.querySelector("#calltable thead"); + let tr = document.createElement("tr"); + + makeCell("Function", tr, "th"); + + let scoreCell = makeCell("Score", tr, "th"); + scoreCell.className = "sort-default score"; + scoreCell.id = "calltable-score-header"; + + let keys = []; + for (let i = 0; i < scoreColumns.length; i++) { + let c = scoreColumns[i]; + let cell = makeCell(c.name, tr, "th"); + if (c.description) { + cell.dataset["title"] = c.description; + cell.addEventListener("mouseover", (evt) => tooltip.showWithDelay("", evt.target, "top", 100)); + cell.addEventListener("mouseout", (evt) => tooltip.hide()); + } + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = true; + checkbox.className = "score-checkbox"; + checkbox.value = i.toString(); + checkbox.addEventListener("change", scoreSelectCallback); + checkbox.addEventListener("click", (evt) => evt.stopPropagation()); // prevent tablesorter from firing + cell.insertAdjacentElement("beforeend", checkbox); + } + + thead.insertAdjacentElement("beforeend", tr); + + // set up configuration controls + let config = document.getElementById("calltable-config")!; + // aggregate checkbox + let agg = config.querySelector("#calltable-aggregate"); + agg.checked = true; + agg.addEventListener("change", configChangeCallback); + // context slider + let ctx = config.querySelector("#calltable-context"); + ctx.value = "0"; + let isIE = !!navigator.userAgent.match(/Trident/g) || !!navigator.userAgent.match(/MSIE/g); + ctx.addEventListener(isIE ? "change" : "input", configChangeCallback); + // context count + let ctxn = config.querySelector("#calltable-context-n"); + ctxn.textContent = "0"; + // filter checkbox + let fil = config.querySelector("#calltable-prune"); + fil.checked = true; + fil.addEventListener("change", configChangeCallback); + // collapse rosette checkbox + let clr = config.querySelector("#calltable-collapse-rosette"); + clr.checked = false; + clr.addEventListener("change", configChangeCallback); + // collapse solver checkbox + let cls = config.querySelector("#calltable-collapse-solver"); + cls.checked = false; + cls.addEventListener("change", configChangeCallback); + // signature checkbox + let sig = config.querySelector("#calltable-signature"); + sig.checked = false; + sig.addEventListener("change", configChangeCallback); + // callsites checkbox + let css = config.querySelector("#calltable-callsites"); + css.checked = false; + css.parentElement.style.display = "none"; + css.addEventListener("change", configChangeCallback); + // score checkbox + let sco = config.querySelector("#calltable-show-scoreboxes"); + sco.checked = false; + sco.addEventListener("change", configChangeCallback); + // more config + let more = config.querySelector("#calltable-config-more"); + more.style.display = "none"; + let moreLink = config.querySelector("#calltable-config-toggle-more"); + moreLink.addEventListener("click", toggleMoreCallback); + + // attach event handler for table body + let tbody = document.querySelector("#calltable tbody"); + tbody.addEventListener("mouseover", hoverCallback); + tbody.addEventListener("mouseout", hoverCallback); + + tableSorter = new Tablesort(document.getElementById("calltable"), { descending: true }); + } + + function configChangeCallback(evt: Event) { + let elt: HTMLInputElement = this; + if (elt.id == "calltable-aggregate") { + let lbl = document.getElementById("calltable-callsites").parentElement; + lbl.style.display = (elt.checked ? "none" : ""); + analysis.setAggregate(elt.checked); + } else if (elt.id == "calltable-context") { + contextDepth = elt.value == elt.max ? -1 : parseInt(elt.value); + let ctxn = document.getElementById("calltable-context-n"); + ctxn.textContent = contextDepth >= 0 ? elt.value : "∞"; + analysis.setContextDepth(contextDepth); + } else if (elt.id == "calltable-prune") { + pruneSmallRows = elt.checked; + analysis.refresh(); + } else if (elt.id == "calltable-collapse-rosette") { + analysis.setCollapseRosette(elt.checked); + } else if (elt.id == "calltable-collapse-solver") { + stackgraph.setCollapseSolverTime(elt.checked); + analysis.setCollapseSolver(elt.checked); + } else if (elt.id == "calltable-signature") { + useSignatures = elt.checked; + analysis.setSignatures(elt.checked); + } else if (elt.id == "calltable-callsites") { + useCallsites = elt.checked; + analysis.refresh(); + } else if (elt.id == "calltable-show-scoreboxes") { + let boxes = >document.querySelectorAll("#calltable .score-checkbox"); + for (let i = 0; i < boxes.length; i++) { + boxes[i].style.display = elt.checked ? "initial" : "none"; + if (!elt.checked) { + scoreColumns[i].score = true; + } + } + if (!elt.checked) { + analysis.setColumns(scoreColumns); + } + } + windowResizeCallback(); + } + + function toggleMoreCallback(evt: Event) { + evt.preventDefault(); + let more = document.getElementById("calltable-config-more"); + let elt: HTMLAnchorElement = this; + if (more.style.display == "none") { + more.style.display = "block"; + this.textContent = "[Less]"; + } else { + more.style.display = "none"; + this.textContent = "[More]"; + } + } + + function hoverCallback(evt: Event) { + if (evt.type == "mouseover") { + let tgt = evt.target; + while (tgt && tgt.tagName != "TR") tgt = tgt.parentElement; + + stackgraph.calltableHoverCallback(tgt[DOM_ROW_KEY].allNodes); + } + else { + stackgraph.calltableHoverCallback([]); + } + } + + function scoreSelectCallback(evt: Event) { + let elt: HTMLInputElement = this; + let idx = parseInt(elt.value); + scoreColumns[idx].score = elt.checked; + analysis.setColumns(scoreColumns); + } + + export function stackgraphHoverCallback(node: CallNode, enter: boolean) { + let rows = document.querySelectorAll("#calltable tbody tr"); + for (let i = 0; i < rows.length; i++) { + let row = rows[i]; + row.className = ""; + } + if (enter) { + let hiRow = idToRow[node.id]; + if (hiRow) { + hiRow.className = "calltable-highlight"; + } + } + } + + function receiveData(state: analysis.ProfileData) { + renderTableRows(state.rows, state.aggregate, state.maxScore); + } + + function renderPrettySource(source: string, cell: HTMLElement, iscallsite=false) { + if (source) { + let prettySource = getPrettySource(source); + if (iscallsite) prettySource = "from " + prettySource; + let sourceSpan = makeCell(prettySource, cell, "span"); + sourceSpan.className = "source"; + sourceSpan.title = source; + } + } + + function getPrettySource(source: string) { + let sourceParts = source.split(":"); + if (sourceParts.length != 3) return source; + let [file, line, col] = sourceParts; + let fileParts = file.split("/"); + return fileParts[fileParts.length-1] + ":" + line; + } + + function renderTableRows(rows: analysis.AnalysisRow[], aggregate: boolean, maxScore: number) { + // remove old rows + let tbody = document.querySelector("#calltable tbody"); + while (tbody.firstChild) tbody.removeChild(tbody.firstChild); + idToRow = {}; + + // create new rows + for (let r of rows) { + let row = document.createElement("tr"); + + if (pruneSmallRows && r.score < maxScore*PRUNE_SCORE_FACTOR) { + continue; + } + + // render the function name + let nameCell = makeCell(r.function, row); + nameCell.className = "name"; + if (useCallsites && !aggregate) { + renderPrettySource(r.node.callsite, nameCell, true); + } else { + renderPrettySource(r.node.source, nameCell); + } + if (aggregate) { + let txt = r.allNodes.length > 1 ? formatNum(r.allNodes.length) + " calls" : "1 call"; + let numCallsSpan = makeCell(txt, nameCell, "span"); + numCallsSpan.className = "numcalls"; + } + + // maybe render the signature + if (useSignatures) { + let inputs: string[] = r.node.inputs["signature"] || []; + let outputs: string[] = r.node.outputs["signature"] || []; + let istr = inputs.map((s) => s[0].toUpperCase()).join(""); + let ostr = outputs.map((s) => s[0].toUpperCase()).join(""); + let span = makeCell(istr + "→" + ostr, nameCell, "span"); + span.className = "signature"; + } + + // render the call context if requested + if (contextDepth != 0) { + let contextList = document.createElement("ul"); + contextList.className = "context-list"; + let n = contextDepth, curr = r.node; + if (n == -1) { // if "infinite", collapse recursion + let count = 0; + while (curr = curr.parent) { + if (curr.parent && curr.parent.name == curr.name && curr.parent.source == curr.source) { + count += 1; + } else { + let name = curr.name + (count > 0 ? " ×" + (count+1) : ""); + let li = makeCell(name, contextList, "li"); + count = 0; + renderPrettySource(curr.source, li); + } + } + } else { + while (n-- != 0 && (curr = curr.parent)) { + let li = makeCell(curr.name, contextList, "li"); + renderPrettySource(curr.source, li); + } + } + nameCell.insertAdjacentElement("beforeend", contextList); + } + + // score cell + let scoreBar = document.createElement("div"); + scoreBar.className = "scorebar"; + scoreBar.style.width = (r.score/scoreColumns.length * 100) + "%"; + scoreBar.style.backgroundColor = colorScheme(r.score); + let scoreBarCell = document.createElement("div"); + scoreBarCell.className = "scorecell-bar"; + scoreBarCell.insertAdjacentElement("beforeend", scoreBar); + let scoreSpan = document.createElement("span"); + scoreSpan.textContent = formatNum(r.score, 1, false, true); + scoreSpan.style.color = colorScheme(r.score); + let scoreSpanCell = document.createElement("div"); + scoreSpanCell.className = "scorecell-score"; + scoreSpanCell.insertAdjacentElement("beforeend", scoreSpan); + let scoreCell = makeCell("", row); + scoreCell.dataset["sort"] = r.score.toFixed(16); + scoreCell.insertAdjacentElement("beforeend", scoreSpanCell); + scoreCell.insertAdjacentElement("beforeend", scoreBarCell); + + // data columns + for (let k of r.columns) { + makeCell(formatNum(k, 0), row); + } + + // attach row object to the row + row[DOM_ROW_KEY] = r; + + // record IDs + for (let n of r.allNodes) { + idToRow[n.id] = row; + } + + tbody.insertAdjacentElement("beforeend", row); + } + + // refresh the sort + tableSorter.refresh(); + } + + function makeCell(str: string, row: HTMLElement, type: string = "td"): HTMLElement { + let elt = document.createElement(type); + elt.textContent = str; + row.insertAdjacentElement('beforeend', elt); + return elt; + } + } + + namespace stackgraph { + var stackgraph: d3_stackgraph.StackGraph; + const colorScheme = makeScoreColorScheme(["#ffeda0","#fed976","#feb24c","#fd8d3c","#fc4e2a","#e31a1c","#bd0026"]); + var solverHighlightColor = "#E3F2FF"; + var lastWidth = 0; + var useDiscontinuities = false; + + export function initStackGraph() { + // build stackgraph + const STACK_WIDTH = document.getElementById("stackgraph")!.clientWidth; + const STACK_HEIGHT = 270; + lastWidth = STACK_WIDTH; + stackgraph = d3_stackgraph.stackGraph("#stackgraph"); + stackgraph.width(STACK_WIDTH).height(STACK_HEIGHT); + stackgraph.clickHandler(clickCallback); + stackgraph.hoverHandler(hoverCallback); + stackgraph.color(nodeColorCallback); + stackgraph.textColor(nodeTextColorCallback); + stackgraph.render(); + + analysis.registerAnalysisCallback(receiveData); + analysis.registerZoomCallback(receiveZoom); + } + + function makeHighlightList(state: analysis.ProfileData) { + let ret = []; + for (let call of state.solverCalls) { + let finish = typeof call.finish === "undefined" ? state.root.finish : call.finish; + let dt = formatNum(finish - call.start, 1); + let summary = ""; + if (call.type == data.SolverCallType.SOLVE) { + let sat = typeof call.sat === "undefined" ? " (pending)" : (call.sat ? " (SAT)" : " (UNSAT)"); + summary = `Solver call${sat}: ${dt}ms`; + } else if (call.type == data.SolverCallType.ENCODE) { + summary = `Solver encoding: ${dt}ms`; + } else if (call.type == data.SolverCallType.FINITIZE) { + summary = `Solver finitization: ${dt}ms`; + } + ret.push({ + start: call.start, + finish: finish, + color: solverHighlightColor, + summary: summary + }); + } + return ret; + } + + function makeDiscontinuityList(state: analysis.ProfileData) { + let ret = [] + for (let call of state.solverCalls) { + let finish = typeof call.finish === "undefined" ? state.root.finish : call.finish; + ret.push([call.start, finish]); + } + return ret; + } + + function receiveData(state: analysis.ProfileData) { + if (state.root) stackgraph.data(state.root); + stackgraph.highlights(makeHighlightList(state)); + let discs = useDiscontinuities ? makeDiscontinuityList(state) : []; + stackgraph.discontinuities(discs); + stackgraph.render(); + } + + function receiveZoom(node: CallNode) { + stackgraph.zoom(node); + } + + function clickCallback(node: CallNode): void { + analysis.zoomTo(node); + } + + export function windowResizeCallback() { // caller should handle debouncing + let width = document.getElementById("stackgraph")!.clientWidth; + if (width != lastWidth) { + stackgraph.width(width).render(); + lastWidth = width; + } + } + + export function calltableHoverCallback(nodes: CallNode[]): void { + stackgraph.highlightData(nodes); + } + + export function setCollapseSolverTime(use: boolean) { + useDiscontinuities = use; + } + + function hoverCallback(node: CallNode, enter: boolean): void { + calltable.stackgraphHoverCallback(node, enter); + } + + function nodeColorCallback(node: CallNode): string { + return colorScheme(node.score); + } + function nodeTextColorCallback(node: CallNode): string { + let col = d3.color(colorScheme(node.score)).rgb(); + let yiq = ((col.r*299) + (col.g*587) + (col.b*114)) / 1000; + return (yiq >= 128) ? "#222222" : "#eeeeee"; + } + } + + + function formatNum(v: number, places: number = 6, sig: boolean = false, always: boolean = false): string { + let pl = sig ? (v < 10 ? 1 : 0) : places; + return (v % 1 == 0 && !always) ? v.toString() : v.toFixed(pl); + } + + + var metadataSet = false; + function setMetadata(state: ProfileState) { + if (!metadataSet && state.metadata != null) { + document.getElementById("profile-source").textContent = state.metadata["source"]; + document.getElementById("profile-time").textContent = state.metadata["time"]; + document.title = "Profile for " + state.metadata["source"] + " generated at " + state.metadata["time"]; + metadataSet = true; + } + } + + + var spinnerVisible = false; + function toggleStreamingSpinner(state: ProfileState) { + let elt = document.getElementById("progress"); + if (state.streaming && !spinnerVisible) { + elt.style.display = "block"; + spinnerVisible = true; + } else if (!state.streaming && spinnerVisible) { + elt.style.display = "none"; + spinnerVisible = false; + } + } + + + var resizing = false; + function windowResizeCallback() { + if (!resizing) { + resizing = true; + stackgraph.windowResizeCallback(); + window.setTimeout(() => { + resizing = false; + }, 50); + } + } + + + // make a color scheme reflecting that scores > 4 are "very bad" + function makeScoreColorScheme(colors: string[]) { + let cs = d3.interpolateRgbBasis(colors); + return function(x) { + if (x > 4.0) return colors[colors.length-1]; + return cs(x/4.0); + }; + } + + + function bindHelpEventHandlers() { + var helps = document.querySelectorAll(".help"); + for (var i = 0; i < helps.length; i++) { + let elt = helps[i]; + elt.addEventListener("mouseover", (evt) => tooltip.showWithDelay("", evt.target, "top", 100)); + elt.addEventListener("mouseout", (evt) => tooltip.hide()); + } + } + + + function init() { + // set up tooltips + tooltip.init(); + bindHelpEventHandlers(); + + // set up analysis + analysis.init(); + + // initialize UI components + stackgraph.initStackGraph(); + calltable.initCallTable(); + + // set up UI callbacks for data state changes + data.registerUpdateCallback(setMetadata); + data.registerUpdateCallback(toggleStreamingSpinner); + + + // receive all the data + data.readyForData(); + + + // set initial widths correctly now that data is rendered + windowResizeCallback(); + window.addEventListener("resize", windowResizeCallback); + } + + document.addEventListener("DOMContentLoaded", init); +} \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/tablesort.js b/rosette/lib/profile/renderer/report/html/js/tablesort.js new file mode 100644 index 00000000..ebaa5380 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/tablesort.js @@ -0,0 +1,296 @@ +/*! + * tablesort v4.0.1 (2016-07-23) + * http://tristen.ca/tablesort/demo/ + * Copyright (c) 2016 ; Licensed MIT +*/ +;(function() { + function Tablesort(el, options) { + if (!(this instanceof Tablesort)) return new Tablesort(el, options); + + if (!el || el.tagName !== 'TABLE') { + throw new Error('Element must be a table'); + } + this.init(el, options || {}); + } + + var sortOptions = []; + + var createEvent = function(name) { + var evt; + + if (!window.CustomEvent || typeof window.CustomEvent !== 'function') { + evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(name, false, false, undefined); + } else { + evt = new CustomEvent(name); + } + + return evt; + }; + + var getInnerText = function(el) { + return el.getAttribute('data-sort') || el.textContent || el.innerText || ''; + }; + + // Default sort method if no better sort method is found + var caseInsensitiveSort = function(a, b) { + a = a.toLowerCase(); + b = b.toLowerCase(); + + if (a === b) return 0; + if (a < b) return 1; + + return -1; + }; + + // Stable sort function + // If two elements are equal under the original sort function, + // then there relative order is reversed + var stabilize = function(sort, antiStabilize) { + return function(a, b) { + var unstableResult = sort(a.td, b.td); + + if (unstableResult === 0) { + if (antiStabilize) return b.index - a.index; + return a.index - b.index; + } + + return unstableResult; + }; + }; + + Tablesort.extend = function(name, pattern, sort) { + if (typeof pattern !== 'function' || typeof sort !== 'function') { + throw new Error('Pattern and sort must be a function'); + } + + sortOptions.push({ + name: name, + pattern: pattern, + sort: sort + }); + }; + + Tablesort.prototype = { + + init: function(el, options) { + var that = this, + firstRow, + defaultSort, + i, + cell; + + that.table = el; + that.thead = false; + that.options = options; + + if (el.rows && el.rows.length > 0) { + if (el.tHead && el.tHead.rows.length > 0) { + for (i = 0; i < el.tHead.rows.length; i++) { + if (el.tHead.rows[i].classList.contains("sort-row")) { + firstRow = el.tHead.rows[i]; + break; + } + } + if (!firstRow) { + firstRow = el.tHead.rows[el.tHead.rows.length - 1]; + } + that.thead = true; + } else { + firstRow = el.rows[0]; + } + } + + if (!firstRow) return; + + var onClick = function() { + if (that.current && that.current !== this) { + that.current.classList.remove('sort-up'); + that.current.classList.remove('sort-down'); + } + + that.current = this; + that.sortTable(this); + }; + + // Assume first row is the header and attach a click handler to each. + for (i = 0; i < firstRow.cells.length; i++) { + cell = firstRow.cells[i]; + if (!cell.classList.contains('no-sort')) { + cell.classList.add('sort-header'); + cell.tabindex = 0; + cell.addEventListener('click', onClick, false); + + if (cell.classList.contains('sort-default')) { + defaultSort = cell; + } + } + } + + if (defaultSort) { + that.current = defaultSort; + that.sortTable(defaultSort); + } + }, + + sortTable: function(header, update) { + var that = this, + column = header.cellIndex, + sortFunction = caseInsensitiveSort, + item = '', + items = [], + i = that.thead ? 0 : 1, + sortDir, + sortMethod = header.getAttribute('data-sort-method'), + sortOrder = header.getAttribute('data-sort-order'); + + that.table.dispatchEvent(createEvent('beforeSort')); + + // If updating an existing sort `sortDir` should remain unchanged. + if (update) { + sortDir = header.classList.contains('sort-up') ? 'sort-up' : 'sort-down'; + } else { + if (header.classList.contains('sort-up')) { + sortDir = 'sort-down'; + } else if (header.classList.contains('sort-down')) { + sortDir = 'sort-up'; + } else if (sortOrder === 'asc') { + sortDir = 'sort-down'; + } else if (sortOrder === 'desc') { + sortDir = 'sort-up'; + } else { + sortDir = that.options.descending ? 'sort-up' : 'sort-down'; + } + + header.classList.remove(sortDir === 'sort-down' ? 'sort-up' : 'sort-down'); + header.classList.add(sortDir); + } + + if (that.table.rows.length < 2) return; + + // If we force a sort method, it is not necessary to check rows + if (!sortMethod) { + while (items.length < 3 && i < that.table.tBodies[0].rows.length) { + item = getInnerText(that.table.tBodies[0].rows[i].cells[column]); + item = item.trim(); + + if (item.length > 0) { + items.push(item); + } + + i++; + } + + if (!items) return; + } + + for (i = 0; i < sortOptions.length; i++) { + item = sortOptions[i]; + + if (sortMethod) { + if (item.name === sortMethod) { + sortFunction = item.sort; + break; + } + } else if (items.every(item.pattern)) { + sortFunction = item.sort; + break; + } + } + + that.col = column; + + for (i = 0; i < that.table.tBodies.length; i++) { + var newRows = [], + noSorts = {}, + j, + totalRows = 0, + noSortsSoFar = 0; + + if (that.table.tBodies[i].rows.length < 2) continue; + + for (j = 0; j < that.table.tBodies[i].rows.length; j++) { + item = that.table.tBodies[i].rows[j]; + if (item.classList.contains('no-sort')) { + // keep no-sorts in separate list to be able to insert + // them back at their original position later + noSorts[totalRows] = item; + } else { + // Save the index for stable sorting + newRows.push({ + tr: item, + td: getInnerText(item.cells[that.col]), + index: totalRows + }); + } + totalRows++; + } + // Before we append should we reverse the new array or not? + // If we reverse, the sort needs to be `anti-stable` so that + // the double negatives cancel out + if (sortDir === 'sort-down') { + newRows.sort(stabilize(sortFunction, true)); + newRows.reverse(); + } else { + newRows.sort(stabilize(sortFunction, false)); + } + + // append rows that already exist rather than creating new ones + for (j = 0; j < totalRows; j++) { + if (noSorts[j]) { + // We have a no-sort row for this position, insert it here. + item = noSorts[j]; + noSortsSoFar++; + } else { + item = newRows[j - noSortsSoFar].tr; + } + + // appendChild(x) moves x if already present somewhere else in the DOM + that.table.tBodies[i].appendChild(item); + } + } + + that.table.dispatchEvent(createEvent('afterSort')); + }, + + refresh: function() { + if (this.current !== undefined) { + this.sortTable(this.current, true); + } + } + }; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = Tablesort; + } else { + window.Tablesort = Tablesort; + } +})(); + +// number sort +(function(){ + var cleanNumber = function(i) { + return i.replace(/[^\-?0-9.]/g, ''); + }, + + compareNumber = function(a, b) { + a = parseFloat(a); + b = parseFloat(b); + + a = isNaN(a) ? 0 : a; + b = isNaN(b) ? 0 : b; + + return a - b; + }; + + Tablesort.extend('number', function(item) { + return item.match(/^-?[£\x24Û¢´€]?\d+\s*([,\.]\d{0,2})/) || // Prefixed currency + item.match(/^-?\d+\s*([,\.]\d{0,2})?[£\x24Û¢´€]/) || // Suffixed currency + item.match(/^-?(\d)*-?([,\.]){0,1}-?(\d)+([E,e][\-+][\d]+)?%?$/); // Number + }, function(a, b) { + a = cleanNumber(a); + b = cleanNumber(b); + + return compareNumber(b, a); + }); +}()); \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/tooltip.js b/rosette/lib/profile/renderer/report/html/js/tooltip.js new file mode 100644 index 00000000..09ab4f40 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/tooltip.js @@ -0,0 +1,81 @@ +var tooltip; +(function (tooltip) { + var tooltipDiv; + var PADDING = 4; + var willShow = false; + function init() { + tooltipDiv = document.createElement("div"); + tooltipDiv.className = "tooltip"; + tooltipDiv.style.position = "absolute"; + tooltipDiv.style.top = "0px"; + tooltipDiv.style.left = "0px"; + tooltipDiv.style.display = "none"; + document.body.insertAdjacentElement("beforeend", tooltipDiv); + } + tooltip.init = init; + function show(title, elt, side) { + if (side != "top") { + console.error("only 'top' side tooltip supported"); + return; + } + if (title == "") { + title = elt.dataset["title"]; + } + tooltipDiv.innerText = title; + tooltipDiv.style.display = "block"; + var tRect = tooltipDiv.getBoundingClientRect(); + var eRect = getBoundingViewportRect(elt); + var vRect = getViewportRect(); + var x = eRect.left + eRect.width / 2 - tRect.width / 2; + if (x < vRect.left) { + x = vRect.left + PADDING; + } + else if (x + tRect.width > vRect.right) { + x = vRect.right - tRect.width - PADDING; + } + var y = eRect.top - tRect.height - PADDING; + if (y < vRect.top) { + y = vRect.top + PADDING; + } + else if (y + tRect.height > vRect.bottom) { + y = vRect.bottom - tRect.height - PADDING; + } + tooltipDiv.style.transform = "translate(" + x + "px," + y + "px)"; + willShow = false; + } + tooltip.show = show; + function showWithDelay(title, elt, side, delay) { + willShow = true; // make sure user didn't leave the element during the delay + setTimeout(function () { if (willShow) + show(title, elt, side); }, delay); + } + tooltip.showWithDelay = showWithDelay; + function getBoundingViewportRect(elt) { + var rect = elt.getBoundingClientRect(); + return { + top: rect.top + window.pageYOffset, + bottom: rect.bottom + window.pageYOffset, + left: rect.left + window.pageXOffset, + right: rect.right + window.pageXOffset, + width: rect.width, + height: rect.height + }; + } + function getViewportRect() { + var rect = document.documentElement.getBoundingClientRect(); + return { + top: window.pageYOffset, + left: window.pageXOffset, + bottom: window.pageYOffset + rect.height, + right: window.pageXOffset + rect.width, + width: rect.width, + height: rect.height + }; + } + function hide() { + tooltipDiv.style.display = "none"; + willShow = false; + } + tooltip.hide = hide; +})(tooltip || (tooltip = {})); +//# sourceMappingURL=tooltip.js.map \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/tooltip.ts b/rosette/lib/profile/renderer/report/html/js/tooltip.ts new file mode 100644 index 00000000..a677179f --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/tooltip.ts @@ -0,0 +1,83 @@ +namespace tooltip { + var tooltipDiv: HTMLDivElement; + const PADDING = 4; + var willShow = false; + + export function init() { + tooltipDiv = document.createElement("div"); + tooltipDiv.className = "tooltip"; + tooltipDiv.style.position = "absolute"; + tooltipDiv.style.top = "0px"; + tooltipDiv.style.left = "0px"; + tooltipDiv.style.display = "none"; + document.body.insertAdjacentElement("beforeend", tooltipDiv); + } + + export function show(title: string, elt: HTMLElement, side: string) { + if (side != "top") { + console.error("only 'top' side tooltip supported"); + return; + } + if (title == "") { + title = elt.dataset["title"]; + } + + tooltipDiv.innerText = title; + tooltipDiv.style.display = "block"; + let tRect = tooltipDiv.getBoundingClientRect(); + let eRect = getBoundingViewportRect(elt); + let vRect = getViewportRect(); + + let x = eRect.left + eRect.width/2 - tRect.width/2; + if (x < vRect.left) { + x = vRect.left + PADDING; + } else if (x + tRect.width > vRect.right) { + x = vRect.right - tRect.width - PADDING; + } + + let y = eRect.top - tRect.height - PADDING; + if (y < vRect.top) { + y = vRect.top + PADDING; + } else if (y + tRect.height > vRect.bottom) { + y = vRect.bottom - tRect.height - PADDING; + } + + tooltipDiv.style.transform = `translate(${x}px,${y}px)`; + + willShow = false; + } + + export function showWithDelay(title: string, elt: HTMLElement, side: string, delay: number) { + willShow = true; // make sure user didn't leave the element during the delay + setTimeout(() => { if (willShow) show(title, elt, side) }, delay); + } + + function getBoundingViewportRect(elt: HTMLElement) { + let rect = elt.getBoundingClientRect(); + return { + top: rect.top + window.pageYOffset, + bottom: rect.bottom + window.pageYOffset, + left: rect.left + window.pageXOffset, + right: rect.right + window.pageXOffset, + width: rect.width, + height: rect.height + }; + } + + function getViewportRect() { + let rect = document.documentElement.getBoundingClientRect(); + return { + top: window.pageYOffset, + left: window.pageXOffset, + bottom: window.pageYOffset + rect.height, + right: window.pageXOffset + rect.width, + width: rect.width, + height: rect.height + }; + } + + export function hide() { + tooltipDiv.style.display = "none"; + willShow = false; + } +} \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/js/tsconfig.json b/rosette/lib/profile/renderer/report/html/js/tsconfig.json new file mode 100644 index 00000000..16449541 --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/js/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "commonjs", + "sourceMap": true + } + } \ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/html/profile.html b/rosette/lib/profile/renderer/report/html/profile.html new file mode 100644 index 00000000..9d8659cc --- /dev/null +++ b/rosette/lib/profile/renderer/report/html/profile.html @@ -0,0 +1,41 @@ +Profile + + + + + + + + + + + + + + + +
+
+
+
+ [More] +
+ +
+
+ + + +
+
+
\ No newline at end of file diff --git a/rosette/lib/profile/renderer/report/solver.rkt b/rosette/lib/profile/renderer/report/solver.rkt new file mode 100644 index 00000000..e84c9be0 --- /dev/null +++ b/rosette/lib/profile/renderer/report/solver.rkt @@ -0,0 +1,41 @@ +#lang racket + +(require "generic.rkt" + "../../data.rkt" "../../reporter.rkt") +(provide make-solver-calls-component) + +(define (make-solver-calls-component options) + (profile-solver-calls-component)) + +(struct profile-solver-calls-component () #:transparent + #:methods gen:report-component + [(define (init-component self) + void) + (define (receive-data self events) + (list (hash 'type "solver-calls" + 'events (get-solver-events events))))]) + +(define (get-solver-events events) + ; get the solver start and finish events in order + (define filtered-events + (filter (lambda (e) (or (profile-event-solve-start? e) + (profile-event-solve-finish? e) + (profile-event-encode? e) + (profile-event-finitize? e))) + events)) + + (for/list ([e filtered-events]) + (let ([time (metrics-ref (profile-event-metrics e) 'time)]) + (match e + [(profile-event-solve-start _) + (hash 'part "solver" 'type "start" 'time time)] + [(profile-event-solve-finish _ sat?) + (hash 'part "solver" 'type "finish" 'time time 'sat sat?)] + [(profile-event-encode-start _) + (hash 'part "encode" 'type "start" 'time time)] + [(profile-event-encode-finish _) + (hash 'part "encode" 'type "finish" 'time time)] + [(profile-event-finitize-start _) + (hash 'part "finitize" 'type "start" 'time time)] + [(profile-event-finitize-finish _) + (hash 'part "finitize" 'type "finish" 'time time)])))) diff --git a/rosette/lib/profile/renderer/report/terms.rkt b/rosette/lib/profile/renderer/report/terms.rkt new file mode 100644 index 00000000..7469e988 --- /dev/null +++ b/rosette/lib/profile/renderer/report/terms.rkt @@ -0,0 +1,64 @@ +#lang racket + +(require (only-in rosette/base/core/term expression) + "generic.rkt" + "../../data.rkt" "../../reporter.rkt") +(provide make-terms-component) + +; The terms component analyzes the program's use of symbolic terms. +; Right now, the only analysis is to see which terms never reach the solver. + +(define (make-terms-component options) + (profile-terms-component (make-hasheq) (make-hasheq) '() (mutable-seteq))) + +(struct profile-terms-component (term->term-node term->creator [stack #:mutable] reached-solver) #:transparent + #:methods gen:report-component + [(define (init-component self) + void) + (define (receive-data self events) + (get-term-analysis-messages self events))]) + +; a node in the term graph contains the term itself, its subterms (as nodes), and its creator +(struct term-node (term subterms creator)) + +; return a list of messages relating to terms -- which are unused etc +(define (get-term-analysis-messages cmpt events) + (match-define (profile-terms-component term->term-node term->creator stack reached-solver) cmpt) + + (for ([e (in-list events)]) + (match e + [(profile-event-enter met id loc proc in) + (set! stack (cons id stack))] + [(profile-event-exit met out) + (set! stack (cdr stack))] + [(profile-event-term-new met t) + (define children ; record all the term's children + (match t + [(expression op elts ...) + (for/list ([e elts] #:when (hash-has-key? term->term-node e)) + (hash-ref term->term-node e))] + [_ '()])) + (define node (term-node t children (car stack))) + (hash-set! term->term-node t node) + (hash-set! term->creator t (car stack))] + [(profile-event-solve-encode met lst) + (for* ([assts lst][a assts] #:when (hash-has-key? term->term-node a)) + (define tn (hash-ref term->term-node a)) + (let loop ([tn tn]) + (unless (set-member? reached-solver tn) + (set-add! reached-solver tn) + (for ([tn* (term-node-subterms tn)]) (loop tn*)))))] + [_ (void)])) + + ; update the call stack after processing these events + (set-profile-terms-component-stack! cmpt stack) + + ; determine the sources of terms that did not reach the solver + (define unused-term-sources (make-hasheq)) + (for ([(t tn) (in-hash term->term-node)] #:unless (set-member? reached-solver tn)) + (define cid (term-node-creator tn)) + (hash-set! unused-term-sources cid (add1 (hash-ref unused-term-sources cid 0)))) + + ; emit that message + (list (hash 'type "unused-terms" + 'data (for/list ([(cid n) (in-hash unused-term-sources)]) (list cid n))))) diff --git a/rosette/lib/profile/renderer/report/ws-server.rkt b/rosette/lib/profile/renderer/report/ws-server.rkt new file mode 100644 index 00000000..4ee12441 --- /dev/null +++ b/rosette/lib/profile/renderer/report/ws-server.rkt @@ -0,0 +1,63 @@ +#lang racket/base + +; this is a patched version of net/rfc6455's server.rkt +; to correctly handle keyword arguments in ws-serve + +;; Convenience interface for starting a simple web service. +;; Roughly compatible with net/websocket/server's ws-serve. + +;; Copyright (c) 2013 Tony Garnock-Jones +;; +;; This module is distributed under the GNU Lesser General Public +;; License (LGPL). This means that you can link it into proprietary +;; applications, provided you follow the rules stated in the LGPL. You +;; can also modify this module; if you distribute a modified version, +;; you must distribute it under the terms of the LGPL, which in +;; particular means that you must release the source code for the +;; modified software. See http://www.gnu.org/licenses/lgpl-3.0.txt for +;; more information. + +(require racket/match) +(require web-server/web-server) +(require web-server/http/request-structs) +(require web-server/http/response) +(require web-server/http/response-structs) +(require web-server/dispatchers/dispatch) +(require net/url) +(require net/rfc6455/dispatcher) +(require net/rfc6455/service-mapper) +(require net/rfc6455/conn-api) +(require (except-in net/rfc6455 ws-serve)) + +(provide ws-serve (all-from-out net/rfc6455)) + +(define (transpose xss) (apply map list xss)) + +(define (guard-dispatcher d) + (lambda (conn req) + (with-handlers [(exn:dispatcher? + (lambda (e) + (log-info "Bad WS request, ~a ~a" + (request-method req) + (url->string (request-uri req))) + (output-response/method + conn + (response 400 #"Bad WebSocket request" (current-seconds) #f '() void) + (request-method req))))] + (d conn req)))) + +(define ws-serve + (procedure-rename + (make-keyword-procedure + (lambda (keys vals conn-dispatch . rest) + (define kvs (map list keys vals)) + (define conn-headers-cell (assq '#:conn-headers kvs)) + (define conn-headers (and conn-headers-cell (cadr conn-headers-cell))) + (define dispatcher (make-general-websockets-dispatcher conn-dispatch conn-headers)) + (match-define (list keys1 vals1) (transpose (remq conn-headers-cell kvs))) + (keyword-apply serve + keys1 + vals1 + rest + #:dispatch (guard-dispatcher dispatcher)))) + 'ws-serve)) diff --git a/rosette/lib/profile/renderer/syntax.rkt b/rosette/lib/profile/renderer/syntax.rkt new file mode 100644 index 00000000..712bf5bd --- /dev/null +++ b/rosette/lib/profile/renderer/syntax.rkt @@ -0,0 +1,48 @@ +#lang racket + +(require racket/date racket/path) +(provide make-folder-name syntax-srcloc procedure-name) + +; Helpers to construct filenames +(define initial-date + (match-let ([pad (lambda (x n) (~r x #:min-width n #:pad-string "0"))] + [(date s m h d M y _ _ _ _) (current-date)]) + (string-append (pad y 4) (pad M 2) (pad d 2) (pad h 2) (pad m 2) (pad s 2)))) +(define (syntax-srcfile stx) + (match stx + [(and (? syntax?) (app syntax-source (? path?))) + (let-values ([(base name dir?) (split-path (syntax-source stx))]) + (path->string name))] + [`(submod (file ,path) ,mod) + (let-values ([(base name dir?) (split-path path)]) + (format "~a-~a" name mod))] + [`(file ,path) + (let-values ([(base name dir?) (split-path path)]) + (path->string name))] + [(? path-string?) + (let-values ([(base name dir?) (split-path stx)]) + (path->string name))] + [_ "unknown"])) +(define make-folder-name + (let ([n 0]) + (lambda (source) + (begin0 + (format "~a-~a-~v" (syntax-srcfile source) initial-date n) + (set! n (+ n 1)))))) + +; Get a version of a syntax object's source location that can be rendered +(define (path->pretty-path path) + (path->string (find-relative-path (current-directory) path))) +(define (syntax-srcloc stx) + (cond [(and (syntax? stx) (path? (syntax-source stx))) + (format "~a:~v:~v" (path->pretty-path (syntax-source stx)) (syntax-line stx) (syntax-column stx))] + [(and (list? stx) (= (length stx) 3) (not (eq? (first stx) 'submod))) + (match-let* ([(list src line col) stx] + [name (if (path? src) (path->pretty-path src) (~a src))]) + (format "~a:~v:~v" name line col))] + [stx (~a stx)] + [else stx])) + +; Get the name of a procedure +(define (procedure-name proc) + (~a (or (object-name proc) proc))) \ No newline at end of file diff --git a/rosette/lib/profile/renderer/trace.rkt b/rosette/lib/profile/renderer/trace.rkt new file mode 100644 index 00000000..69722b56 --- /dev/null +++ b/rosette/lib/profile/renderer/trace.rkt @@ -0,0 +1,33 @@ +#lang racket + +(require "../data.rkt" "../record.rkt" "../graph.rkt" "../reporter.rkt" + "renderer.rkt" + "syntax.rkt") + +(provide make-trace-renderer) + +(struct trace-renderer (source name) + #:transparent + #:methods gen:renderer + [(define start-renderer void) + (define (finish-renderer self profile) + (match-define (trace-renderer source name) self) + (render-trace (profile-state->graph profile) source name))]) + +(define (make-trace-renderer source name [options (hash)]) + (trace-renderer source name)) + +(define (render-trace profile source name) + (define (indent n) + (string-join (for/list ([i n]) " ") "")) + (printf "Trace for ~a (~v)\n" name source) + (let rec ([node profile][level 0]) + (define metrics (profile-data-metrics (profile-node-data node))) + (printf "~a* ~a (~v ms, ~v merges, ~v unions, ~v terms)\n" + (indent level) (procedure-name (profile-data-procedure (profile-node-data node))) + (metrics-ref metrics 'time) + (metrics-ref metrics 'merge-count) + (metrics-ref metrics 'union-count) + (metrics-ref metrics 'term-count)) + (for ([c (profile-node-children node)]) + (rec c (add1 level))))) diff --git a/rosette/lib/profile/reporter.rkt b/rosette/lib/profile/reporter.rkt new file mode 100644 index 00000000..58be5e1c --- /dev/null +++ b/rosette/lib/profile/reporter.rkt @@ -0,0 +1,116 @@ +#lang racket + +(require rosette/base/core/reporter "data.rkt") +(provide (struct-out profiler-reporter) make-profiler-reporter + get-current-metrics/call get-call-time + get-sample-event + metrics-ref diff-metrics metrics->hash) + +; The profiler reporter keeps a cumulative count of several metrics, +; as an association list, and reports +; them when requested to insert into a profile node. +; (Performance note: an association list is slightly faster than a hash table +; for workloads that clone the current metrics state a lot, such as MemSynth). +(define (make-profiler-reporter profile) + (profiler-reporter + profile + (map (curryr cons 0) '(term-count merge-count merge-cases union-count union-size)) + #f)) + +(struct profiler-reporter (profile [metrics #:mutable] [finitizing #:mutable]) + #:transparent + #:property prop:procedure + (lambda (self . rest) + (match rest + [(list 'new-term the-term) + (unless (profiler-reporter-finitizing self) + (inc! self 'term-count 1)) + (let* ([new (profile-event-term-new (get-current-metrics/none) the-term)]) + (profile-state-append! (profiler-reporter-profile self) new))] + [(list 'merge merge-cases) + (unless (profiler-reporter-finitizing self) + (inc! self 'merge-count 1) + (inc! self 'merge-cases merge-cases))] + [(list 'new-union union-size) + (unless (profiler-reporter-finitizing self) + (inc! self 'union-count 1) + (inc! self 'union-size union-size))] + [(list 'solve-start) + (let* ([new (profile-event-solve-start (get-current-metrics/event))]) + (profile-state-append! (profiler-reporter-profile self) new))] + [(list 'solve-finish sat?) + (let* ([new (profile-event-solve-finish (get-current-metrics/event) sat?)]) + (profile-state-append! (profiler-reporter-profile self) new))] + [(list 'to-solver lists ...) + (let* ([new (profile-event-solve-encode (get-current-metrics/none) lists)]) + (profile-state-append! (profiler-reporter-profile self) new))] + [(list 'finitize-start) + (set-profiler-reporter-finitizing! self #t) + (let* ([new (profile-event-finitize-start (get-current-metrics/event))]) + (profile-state-append! (profiler-reporter-profile self) new))] + [(list 'finitize-finish) + (set-profiler-reporter-finitizing! self #f) + (let* ([new (profile-event-finitize-finish (get-current-metrics/event))]) + (profile-state-append! (profiler-reporter-profile self) new))] + [(list 'encode-start) + (let* ([new (profile-event-encode-start (get-current-metrics/event))]) + (profile-state-append! (profiler-reporter-profile self) new))] + [(list 'encode-finish) + (let* ([new (profile-event-encode-finish (get-current-metrics/event))]) + (profile-state-append! (profiler-reporter-profile self) new))] + [_ void]))) + + +(define (assoc-inc xs x v) + (let loop ([xs xs]) + (cond [(null? xs) (cons x v)] + [(eq? (caar xs) x) (cons (cons x (+ v (cdar xs))) (cdr xs))] + [else (cons (car xs) (loop (cdr xs)))]))) +(define (assoc-dec xs x v) + (let loop ([xs xs]) + (cond [(null? xs) (cons x v)] + [(eq? (caar xs) x) (cons (cons x (- (cdar xs) v)) (cdr xs))] + [else (cons (car xs) (loop (cdr xs)))]))) + +(define-syntax-rule (inc! reporter key val) + (let ([ht (profiler-reporter-metrics reporter)]) + (set-profiler-reporter-metrics! reporter (assoc-inc ht key val)))) +(define-syntax-rule (dec! reporter key val) + (let ([ht (profiler-reporter-metrics reporter)]) + (set-profiler-reporter-metrics! reporter (assoc-dec ht key val)))) + + +(define (get-current-metrics/event) + (list (cons 'time (current-inexact-milliseconds)))) +(define (get-current-metrics/none) + '()) +(define (get-current-metrics/call reporter) + (cons (cons 'time (current-inexact-milliseconds)) + (profiler-reporter-metrics reporter))) +; shortcut to get time from a get-current-metrics/call instance; +; make sure to update if get-current-metrics/call changes +(define (get-call-time evt) + (cdar (profile-event-metrics evt))) + + +(define (get-sample-event) + (profile-event-sample (get-current-metrics/call (current-reporter)))) + + +;; Abstract out references to metrics in case we decide we need a better data +;; structure at some point. +(define (metrics-ref mets key) + (let ([a (assq key mets)]) + (if a (cdr a) #f))) + + +;; Helper to compute the difference between entry and exit metrics +(define (diff-metrics old new) + (for/list ([k/v new]) + (let ([k (car k/v)][v (cdr k/v)]) + (let ([o (assq k old)]) + (cons k (- v (if o (cdr o) 0))))))) + +;; Convert metrics to a hash for output +(define (metrics->hash m) + (for/hash ([k/v m]) (values (car k/v) (cdr k/v)))) diff --git a/rosette/lib/profile/tool.rkt b/rosette/lib/profile/tool.rkt new file mode 100644 index 00000000..d6c7fdb9 --- /dev/null +++ b/rosette/lib/profile/tool.rkt @@ -0,0 +1,33 @@ +#lang racket + +(require "data.rkt" "record.rkt" "reporter.rkt" + "renderer/renderer.rkt" + "renderer/noop.rkt") +(provide (all-defined-out)) + +; The selected renderer +(define current-renderer (make-parameter make-noop-renderer)) + +; Executes the given thunk and prints the profile data generated during execution. +(define (profile-thunk thunk #:renderer [renderer% (current-renderer)] + #:source [source-stx #f] + #:name [name "Profile"]) + (define profile (make-profile-state)) + (define reporter (make-profiler-reporter profile)) + (define renderer (renderer% source-stx name)) + (start-renderer renderer profile reporter) + (define ret (run-profile-thunk thunk profile reporter)) + (finish-renderer renderer profile) + (apply values ret)) + + +;; TODO: we probably need a version of profile-thunk etc that does +;; the profiling wrt a clean symbolic state (empty assertion stack, term cache etc). + + +; Profile the given form +(define-syntax (profile stx) + (syntax-case stx () + [(_ expr args ...) + (syntax/loc stx + (profile-thunk (thunk expr) #:source #'expr args ...))])) diff --git a/rosette/query/finitize.rkt b/rosette/query/finitize.rkt index bc5cbc41..d91ddc0d 100644 --- a/rosette/query/finitize.rkt +++ b/rosette/query/finitize.rkt @@ -9,6 +9,7 @@ "../base/core/polymorphic.rkt" "../base/core/merge.rkt" "../base/core/union.rkt" + "../base/core/reporter.rkt" (only-in "../solver/solution.rkt" model core sat unsat sat? unsat?) (only-in "../base/core/term.rkt" [operator-unsafe unsafe])) @@ -32,9 +33,13 @@ ; their subterms, to their corresponding BV finitizations. Terms that are already in BV ; finitize to themselves. (define (finitize terms [bw (current-bitwidth)] [env (make-hash)]) + ; lie to the profiler: any term that gets finitized makes it to the solver + ((current-reporter) 'to-solver terms) (parameterize ([current-bitwidth bw]) + ((current-reporter) 'finitize-start) (for ([t terms]) (finitize-any t env)) + ((current-reporter) 'finitize-finish) env)) ; Takes as input a solution and a finitization map diff --git a/rosette/solver/smt/base-solver.rkt b/rosette/solver/smt/base-solver.rkt index 155059bb..0152202b 100644 --- a/rosette/solver/smt/base-solver.rkt +++ b/rosette/solver/smt/base-solver.rkt @@ -7,7 +7,8 @@ (only-in "../../base/core/term.rkt" term term? term-type) (only-in "../../base/core/bool.rkt" @boolean?) (only-in "../../base/core/bitvector.rkt" bitvector? bv?) - (only-in "../../base/core/real.rkt" @integer? @real?)) + (only-in "../../base/core/real.rkt" @integer? @real?) + (only-in "../../base/core/reporter.rkt" current-reporter)) (provide (all-defined-out)) @@ -71,7 +72,9 @@ (server-write server (begin + ((current-reporter) 'encode-start) (encode env asserts mins maxs) + ((current-reporter) 'encode-finish) (push))) (solver-clear-stacks! self) (set-solver-level! self (cons (dict-count env) level))) @@ -91,10 +94,16 @@ (cond [(ormap false? asserts) (unsat)] [else (server-write server - (begin (encode env asserts mins maxs) - (check-sat))) + (begin + ((current-reporter) 'encode-start) + (encode env asserts mins maxs) + ((current-reporter) 'encode-finish) + (check-sat))) + ((current-reporter) 'solve-start) (solver-clear-stacks! self) - (read-solution server env)])) + (define ret (read-solution server env)) + ((current-reporter) 'solve-finish (sat? ret)) + ret])) (define (solver-debug self) (error 'solver-debug "debugging isn't supported by solver ~v" self)) diff --git a/rosette/solver/smt/cmd.rkt b/rosette/solver/smt/cmd.rkt index e81f42d3..b4cc90f4 100644 --- a/rosette/solver/smt/cmd.rkt +++ b/rosette/solver/smt/cmd.rkt @@ -9,6 +9,7 @@ (only-in "../../base/core/bool.rkt" @boolean?) (only-in "../../base/core/bitvector.rkt" bitvector? bv) (only-in "../../base/core/real.rkt" @integer? @real?) + "../../base/core/reporter.rkt" "../solution.rkt") (provide encode encode-for-proof decode) @@ -22,6 +23,7 @@ ; be augmented, if needed, with additional declarations and ; definitions. This procedure will not emit any other commands. (define (encode env asserts mins maxs) + ((current-reporter) 'to-solver asserts mins maxs) (for ([a asserts]) (assert (enc a env))) (for ([m mins]) diff --git a/sdsl/bv/examples/easy.rkt b/sdsl/bv/examples/easy.rkt index d709b3b9..2ad18e00 100644 --- a/sdsl/bv/examples/easy.rkt +++ b/sdsl/bv/examples/easy.rkt @@ -1,5 +1,6 @@ -#lang s-exp "../bv.rkt" +#lang rosette +(require "../bv.rkt") (require "reference.rkt") (provide (all-defined-out)) diff --git a/sdsl/bv/examples/hard.rkt b/sdsl/bv/examples/hard.rkt index 5bc98258..ee7433b2 100644 --- a/sdsl/bv/examples/hard.rkt +++ b/sdsl/bv/examples/hard.rkt @@ -1,5 +1,6 @@ -#lang s-exp "../bv.rkt" +#lang rosette +(require "../bv.rkt") (require "reference.rkt") (provide (all-defined-out)) diff --git a/sdsl/bv/examples/medium.rkt b/sdsl/bv/examples/medium.rkt index f8847f88..52e42ef0 100644 --- a/sdsl/bv/examples/medium.rkt +++ b/sdsl/bv/examples/medium.rkt @@ -1,5 +1,6 @@ -#lang s-exp "../bv.rkt" +#lang rosette +(require "../bv.rkt") (require "reference.rkt") (provide (all-defined-out)) diff --git a/sdsl/bv/examples/reference.rkt b/sdsl/bv/examples/reference.rkt index cedb53a3..5f34a391 100644 --- a/sdsl/bv/examples/reference.rkt +++ b/sdsl/bv/examples/reference.rkt @@ -1,5 +1,6 @@ -#lang s-exp "../bv.rkt" +#lang rosette +(require "../bv.rkt") (provide (all-defined-out)) ; The 25 Hacker's Delight benchmarks from the following paper: diff --git a/sdsl/bv/test/easy.rkt b/sdsl/bv/test/easy.rkt index e6f575bc..21107840 100644 --- a/sdsl/bv/test/easy.rkt +++ b/sdsl/bv/test/easy.rkt @@ -1,5 +1,6 @@ -#lang s-exp "../bv.rkt" +#lang rosette +(require "../bv.rkt") (require rackunit rackunit/text-ui "util.rkt" rosette/lib/roseunit) (require "../examples/reference.rkt") diff --git a/sdsl/bv/test/medium.rkt b/sdsl/bv/test/medium.rkt index b8618433..3a566898 100644 --- a/sdsl/bv/test/medium.rkt +++ b/sdsl/bv/test/medium.rkt @@ -1,5 +1,6 @@ -#lang s-exp "../bv.rkt" +#lang rosette +(require "../bv.rkt") (require rackunit rackunit/text-ui "util.rkt" rosette/lib/roseunit) (require "../examples/reference.rkt") diff --git a/test/all-rosette-tests.rkt b/test/all-rosette-tests.rkt index 983c1d17..3bae022c 100644 --- a/test/all-rosette-tests.rkt +++ b/test/all-rosette-tests.rkt @@ -40,7 +40,8 @@ "query/solve+.rkt" "query/synthax.rkt" "query/debug.rkt" - "query/optimize.rkt") + "query/optimize.rkt" + "profile/test.rkt") (define (run-tests-with-solver solver%) diff --git a/test/profile/benchmarks/exn.rkt b/test/profile/benchmarks/exn.rkt new file mode 100644 index 00000000..9f047e36 --- /dev/null +++ b/test/profile/benchmarks/exn.rkt @@ -0,0 +1,17 @@ +#lang rosette + +; A simple test to check exception handling. When running the symbolic profiler +; in trace mode, the `add1` invocation in `foo` should not be a child of the +; `raise-argument-error` invocation. If it is, then the profiler stack has +; fallen out of sync with the actual stack due to the exception being thrown. + +(define (foo n) + (if (= n 2) + (raise-argument-error 'foo "not 2" n) + (add1 n))) + +(define (bar) + (define-symbolic i integer?) + (foo i)) + +(bar) diff --git a/test/profile/benchmarks/list.rkt b/test/profile/benchmarks/list.rkt new file mode 100644 index 00000000..3cfef784 --- /dev/null +++ b/test/profile/benchmarks/list.rkt @@ -0,0 +1,12 @@ +#lang rosette + +; Micro-benchmarks for various list operations. + +(require rosette/lib/angelic) +(provide (all-defined-out)) + + +; Construct a symbolic list of up to the given length +(define (symbolic-list len) + (define lst (build-list len identity)) + (apply choose* (for/list ([i len]) (take lst i)))) diff --git a/test/profile/benchmarks/update-at.rkt b/test/profile/benchmarks/update-at.rkt new file mode 100644 index 00000000..952d346f --- /dev/null +++ b/test/profile/benchmarks/update-at.rkt @@ -0,0 +1,25 @@ +#lang rosette + +(require "list.rkt") +(provide (all-defined-out)) + + +(define (update-at lst pos val) + (match lst + [(list) lst] + [(list x xs ...) + (if (= pos 0) + (cons val xs) + (cons x (update-at xs (- pos 1) val)))])) + + +; Simple test for update-at +(define (test-update-at lst) + (define-symbolic* idx integer?) + (update-at lst idx -1) + (void)) + +(define lst (build-list 50 identity)) + + +(time (test-update-at lst)) diff --git a/test/profile/output/micro-exn.out b/test/profile/output/micro-exn.out new file mode 100644 index 00000000..a108c5a7 --- /dev/null +++ b/test/profile/output/micro-exn.out @@ -0,0 +1 @@ +(the-profiled-thunk (bar (foo (=) (raise-argument-error) (@add1)))) \ No newline at end of file diff --git a/test/profile/output/micro-update-at.out b/test/profile/output/micro-update-at.out new file mode 100644 index 00000000..8c6e72f1 --- /dev/null +++ b/test/profile/output/micro-update-at.out @@ -0,0 +1 @@ +(the-profiled-thunk (build-list) (test-update-at (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at (=) (@cons) (-) (update-at) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (@cons)) (void))) \ No newline at end of file diff --git a/test/profile/renderer.rkt b/test/profile/renderer.rkt new file mode 100644 index 00000000..1a555dfb --- /dev/null +++ b/test/profile/renderer.rkt @@ -0,0 +1,53 @@ +#lang racket + +(require rackunit + racket/runtime-path + rosette/lib/profile/data + rosette/lib/profile/graph + rosette/lib/profile/tool + rosette/lib/profile/renderer/renderer + rosette/lib/roseunit + (only-in rosette clear-state!)) +(provide regression-test) + + +(define-runtime-path output-path "output") + + +(struct regression-renderer (path) + #:transparent + #:methods gen:renderer + [(define start-renderer void) + (define (finish-renderer self profile) + (check-profile profile (regression-renderer-path self)))]) + + +; Check whether the profile matches the one saved in `path`. +; If `path` doesn't exist, write the output of the new profile there instead of checking. +(define (check-profile profile path) + (define new-graph (profile-state->graph profile)) + (define new-list + (let loop ([graph new-graph]) + (cons (let ([proc (profile-data-procedure (profile-node-data graph))]) + (or (object-name proc) proc)) + (for/list ([c (profile-node-children graph)]) (loop c))))) + (define outpath (build-path output-path (format "~a.out" path))) + (if (file-exists? outpath) + (let ([old-list (call-with-input-file outpath read)]) + (check-equal? new-list old-list)) + (let () + (with-output-to-file outpath (thunk (write new-list))) + (printf "Wrote new output for `~a`: ~v\n" path new-list)))) + + + +(define-syntax-rule (regression-test test-name path code) + (test-suite+ test-name + (let ([renderer% (lambda (source name [options (hash)]) + (regression-renderer path))] + [ns (make-base-namespace)]) + (clear-state!) + (parameterize ([current-namespace ns]) + (profile-thunk + (lambda () code) + #:renderer renderer%))))) diff --git a/test/profile/test.rkt b/test/profile/test.rkt new file mode 100644 index 00000000..5f73235d --- /dev/null +++ b/test/profile/test.rkt @@ -0,0 +1,39 @@ +#lang racket + +(require rosette/lib/profile/compile + rosette/lib/profile/record + racket/runtime-path + "renderer.rkt") + + +(filtering-threshold 0) + + +; Tests must dynamically require their code so that they are seen by the +; profiler compile handler. +(define (run-profile-regression-test path) + (parameterize ([current-compile symbolic-profile-compile-handler]) + (dynamic-require `(file ,(path->string path)) #f))) + + +; Test exception handling +(define-runtime-path exn.rkt "benchmarks/exn.rkt") +(define micro-exn + (regression-test + "Profiler call graph: benchmarks/exn.rkt" + "micro-exn" + (run-profile-regression-test exn.rkt))) + +; Test list update-at +(define-runtime-path update-at.rkt "benchmarks/update-at.rkt") +(define micro-update-at + (regression-test + "Profiler call graph: benchmarks/update-at.rkt" + "micro-update-at" + (run-profile-regression-test update-at.rkt))) + + +(module+ test + (require rackunit/text-ui) + (run-tests micro-exn) + (run-tests micro-update-at))