Spaces:
Sleeping
Sleeping
;;; -*- 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))) | |