123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- ;;; -*- mode: scheme; coding: utf-8; -*-
- ;;;
- ;;; Copyright (C) 2010 Free Software Foundation, Inc.
- ;;;
- ;;; This library is free software; you can redistribute it and/or
- ;;; modify it under the terms of the GNU Lesser General Public
- ;;; License as published by the Free Software Foundation; either
- ;;; version 3 of the License, or (at your option) any later version.
- ;;;
- ;;; This library is distributed in the hope that it will be useful,
- ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
- ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- ;;; Lesser General Public License for more details.
- ;;;
- ;;; You should have received a copy of the GNU Lesser General Public
- ;;; License along with this library; if not, write to the Free Software
- ;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- (define-module (system vm coverage)
- #:use-module (system vm vm)
- #:use-module (system vm frame)
- #:use-module (system vm program)
- #:use-module (srfi srfi-1)
- #:use-module (srfi srfi-9)
- #:use-module (srfi srfi-11)
- #:use-module (srfi srfi-26)
- #:export (with-code-coverage
- coverage-data?
- instrumented-source-files
- instrumented/executed-lines
- line-execution-counts
- procedure-execution-count
- coverage-data->lcov))
- ;;; Author: Ludovic Courtès
- ;;;
- ;;; Commentary:
- ;;;
- ;;; This module provides support to gather code coverage data by instrumenting
- ;;; the VM.
- ;;;
- ;;; Code:
- ;;;
- ;;; Gathering coverage data.
- ;;;
- (define (hashq-proc proc n)
- ;; Return the hash of PROC's objcode.
- (hashq (program-objcode proc) n))
- (define (assq-proc proc alist)
- ;; Instead of really looking for PROC in ALIST, look for the objcode of PROC.
- ;; IOW the alist is indexed by procedures, not objcodes, but those procedures
- ;; are taken as an arbitrary representative of all the procedures (closures)
- ;; sharing that objcode. This can significantly reduce memory consumption.
- (let ((code (program-objcode proc)))
- (find (lambda (pair)
- (eq? code (program-objcode (car pair))))
- alist)))
- (define (with-code-coverage vm thunk)
- "Run THUNK, a zero-argument procedure, using VM; instrument VM to collect code
- coverage data. Return code coverage data and the values returned by THUNK."
- (define procedure->ip-counts
- ;; Mapping from procedures to hash tables; said hash tables map instruction
- ;; pointers to the number of times they were executed.
- (make-hash-table 500))
- (define (collect! frame)
- ;; Update PROCEDURE->IP-COUNTS with info from FRAME.
- (let* ((proc (frame-procedure frame))
- (ip (frame-instruction-pointer frame))
- (proc-entry (hashx-create-handle! hashq-proc assq-proc
- procedure->ip-counts proc #f)))
- (let loop ()
- (define ip-counts (cdr proc-entry))
- (if ip-counts
- (let ((ip-entry (hashv-create-handle! ip-counts ip 0)))
- (set-cdr! ip-entry (+ (cdr ip-entry) 1)))
- (begin
- (set-cdr! proc-entry (make-hash-table))
- (loop))))))
- ;; FIXME: It's unclear what the dynamic-wind is for, given that if the
- ;; VM is different from the current one, continuations will not be
- ;; resumable.
- (call-with-values (lambda ()
- (let ((level (vm-trace-level vm))
- (hook (vm-next-hook vm)))
- (dynamic-wind
- (lambda ()
- (set-vm-trace-level! vm (+ level 1))
- (add-hook! hook collect!))
- (lambda ()
- (call-with-vm vm thunk))
- (lambda ()
- (set-vm-trace-level! vm level)
- (remove-hook! hook collect!)))))
- (lambda args
- (apply values (make-coverage-data procedure->ip-counts) args))))
- ;;;
- ;;; Coverage data summary.
- ;;;
- (define-record-type <coverage-data>
- (%make-coverage-data procedure->ip-counts
- procedure->sources
- file->procedures
- file->line-counts)
- coverage-data?
- ;; Mapping from procedures to hash tables; said hash tables map instruction
- ;; pointers to the number of times they were executed.
- (procedure->ip-counts data-procedure->ip-counts)
- ;; Mapping from procedures to the result of `program-sources'.
- (procedure->sources data-procedure->sources)
- ;; Mapping from source file names to lists of procedures defined in the file.
- (file->procedures data-file->procedures)
- ;; Mapping from file names to hash tables, which in turn map from line numbers
- ;; to execution counts.
- (file->line-counts data-file->line-counts))
- (define (make-coverage-data procedure->ip-counts)
- ;; Return a `coverage-data' object based on the coverage data available in
- ;; PROCEDURE->IP-COUNTS. Precompute the other hash tables that make up
- ;; `coverage-data' objects.
- (let* ((procedure->sources (make-hash-table 500))
- (file->procedures (make-hash-table 100))
- (file->line-counts (make-hash-table 100))
- (data (%make-coverage-data procedure->ip-counts
- procedure->sources
- file->procedures
- file->line-counts)))
- (define (increment-execution-count! file line count)
- ;; Make the execution count of FILE:LINE the maximum of its current value
- ;; and COUNT. This is so that LINE's execution count is correct when
- ;; several instruction pointers map to LINE.
- (let ((file-entry (hash-create-handle! file->line-counts file #f)))
- (if (not (cdr file-entry))
- (set-cdr! file-entry (make-hash-table 500)))
- (let ((line-entry (hashv-create-handle! (cdr file-entry) line 0)))
- (set-cdr! line-entry (max (cdr line-entry) count)))))
- ;; Update execution counts for procs that were executed.
- (hash-for-each (lambda (proc ip-counts)
- (let* ((sources (program-sources* data proc))
- (file (and (pair? sources)
- (source:file (car sources)))))
- (and file
- (begin
- ;; Add a zero count for all IPs in SOURCES and in
- ;; the sources of procedures closed over by PROC.
- (for-each
- (lambda (source)
- (let ((file (source:file source))
- (line (source:line source)))
- (increment-execution-count! file line 0)))
- (append-map (cut program-sources* data <>)
- (closed-over-procedures proc)))
- ;; Add the actual execution count collected.
- (hash-for-each
- (lambda (ip count)
- (let ((line (closest-source-line sources ip)))
- (increment-execution-count! file line count)))
- ip-counts)))))
- procedure->ip-counts)
- ;; Set the execution count to zero for procedures loaded and not executed.
- ;; FIXME: Traversing thousands of procedures here is inefficient.
- (for-each (lambda (proc)
- (and (not (hashq-ref procedure->sources proc))
- (for-each (lambda (proc)
- (let* ((sources (program-sources* data proc))
- (file (and (pair? sources)
- (source:file (car sources)))))
- (and file
- (for-each
- (lambda (ip)
- (let ((line (closest-source-line sources ip)))
- (increment-execution-count! file line 0)))
- (map source:addr sources)))))
- (closed-over-procedures proc))))
- (append-map module-procedures (loaded-modules)))
- data))
- (define (procedure-execution-count data proc)
- "Return the number of times PROC's code was executed, according to DATA, or #f
- if PROC was not executed. When PROC is a closure, the number of times its code
- was executed is returned, not the number of times this code associated with this
- particular closure was executed."
- (let ((sources (program-sources* data proc)))
- (and (pair? sources)
- (and=> (hashx-ref hashq-proc assq-proc
- (data-procedure->ip-counts data) proc)
- (lambda (ip-counts)
- ;; FIXME: broken with lambda*
- (let ((entry-ip (source:addr (car sources))))
- (hashv-ref ip-counts entry-ip 0)))))))
- (define (program-sources* data proc)
- ;; A memoizing version of `program-sources'.
- (or (hashq-ref (data-procedure->sources data) proc)
- (and (program? proc)
- (let ((sources (program-sources proc))
- (p->s (data-procedure->sources data))
- (f->p (data-file->procedures data)))
- (if (pair? sources)
- (let* ((file (source:file (car sources)))
- (entry (hash-create-handle! f->p file '())))
- (hashq-set! p->s proc sources)
- (set-cdr! entry (cons proc (cdr entry)))
- sources)
- sources)))))
- (define (file-procedures data file)
- ;; Return the list of globally bound procedures defined in FILE.
- (hash-ref (data-file->procedures data) file '()))
- (define (instrumented/executed-lines data file)
- "Return the number of instrumented and the number of executed source lines in
- FILE according to DATA."
- (define instr+exec
- (and=> (hash-ref (data-file->line-counts data) file)
- (lambda (line-counts)
- (hash-fold (lambda (line count instr+exec)
- (let ((instr (car instr+exec))
- (exec (cdr instr+exec)))
- (cons (+ 1 instr)
- (if (> count 0)
- (+ 1 exec)
- exec))))
- '(0 . 0)
- line-counts))))
- (values (car instr+exec) (cdr instr+exec)))
- (define (line-execution-counts data file)
- "Return a list of line number/execution count pairs for FILE, or #f if FILE
- is not among the files covered by DATA."
- (and=> (hash-ref (data-file->line-counts data) file)
- (lambda (line-counts)
- (hash-fold alist-cons '() line-counts))))
- (define (instrumented-source-files data)
- "Return the list of `instrumented' source files, i.e., source files whose code
- was loaded at the time DATA was collected."
- (hash-fold (lambda (file counts files)
- (cons file files))
- '()
- (data-file->line-counts data)))
- ;;;
- ;;; Helpers.
- ;;;
- (define (loaded-modules)
- ;; Return the list of all the modules currently loaded.
- (define seen (make-hash-table))
- (let loop ((modules (module-submodules (resolve-module '() #f)))
- (result '()))
- (hash-fold (lambda (name module result)
- (if (hashq-ref seen module)
- result
- (begin
- (hashq-set! seen module #t)
- (loop (module-submodules module)
- (cons module result)))))
- result
- modules)))
- (define (module-procedures module)
- ;; Return the list of procedures bound globally in MODULE.
- (hash-fold (lambda (binding var result)
- (if (variable-bound? var)
- (let ((value (variable-ref var)))
- (if (procedure? value)
- (cons value result)
- result))
- result))
- '()
- (module-obarray module)))
- (define (closest-source-line sources ip)
- ;; Given SOURCES, as returned by `program-sources' for a given procedure,
- ;; return the source line of code that is the closest to IP. This is similar
- ;; to what `program-source' does.
- (let loop ((sources sources)
- (line (and (pair? sources) (source:line (car sources)))))
- (if (null? sources)
- line
- (let ((source (car sources)))
- (if (> (source:addr source) ip)
- line
- (loop (cdr sources) (source:line source)))))))
- (define (closed-over-procedures proc)
- ;; Return the list of procedures PROC closes over, PROC included.
- (let loop ((proc proc)
- (result '()))
- (if (and (program? proc) (not (memq proc result)))
- (fold loop (cons proc result)
- (append (vector->list (or (program-objects proc) #()))
- (program-free-variables proc)))
- result)))
- ;;;
- ;;; LCOV output.
- ;;;
- (define* (coverage-data->lcov data port)
- "Traverse code coverage information DATA, as obtained with
- `with-code-coverage', and write coverage information in the LCOV format to PORT.
- The report will include all the modules loaded at the time coverage data was
- gathered, even if their code was not executed."
- (define (dump-function proc)
- ;; Dump source location and basic coverage data for PROC.
- (and (program? proc)
- (let ((sources (program-sources* data proc)))
- (and (pair? sources)
- (let* ((line (source:line-for-user (car sources)))
- (name (or (procedure-name proc)
- (format #f "anonymous-l~a" line))))
- (format port "FN:~A,~A~%" line name)
- (and=> (procedure-execution-count data proc)
- (lambda (count)
- (format port "FNDA:~A,~A~%" count name))))))))
- ;; Output per-file coverage data.
- (format port "TN:~%")
- (for-each (lambda (file)
- (let ((procs (file-procedures data file))
- (path (search-path %load-path file)))
- (if (string? path)
- (begin
- (format port "SF:~A~%" path)
- (for-each dump-function procs)
- (for-each (lambda (line+count)
- (let ((line (car line+count))
- (count (cdr line+count)))
- (format port "DA:~A,~A~%"
- (+ 1 line) count)))
- (line-execution-counts data file))
- (let-values (((instr exec)
- (instrumented/executed-lines data file)))
- (format port "LH: ~A~%" exec)
- (format port "LF: ~A~%" instr))
- (format port "end_of_record~%"))
- (begin
- (format (current-error-port)
- "skipping unknown source file: ~a~%"
- file)))))
- (instrumented-source-files data)))
|