]> asedeno.scripts.mit.edu Git - cl-protobufs.git/blobdiff - serialize.lisp
Merge branch 'rework-schema-import-and-lookup'
[cl-protobufs.git] / serialize.lisp
index b5be439fd449bafa1387a489f1ac2e081d2be1d5..cb48a1424ace8582c78a2be7772f293f886c6c35 100644 (file)
@@ -2,7 +2,7 @@
 ;;;                                                                  ;;;
 ;;; Free Software published under an MIT-like license. See LICENSE   ;;;
 ;;;                                                                  ;;;
-;;; Copyright (c) 2012 Google, Inc.  All rights reserved.            ;;;
+;;; Copyright (c) 2012-2013 Google, Inc.  All rights reserved.       ;;;
 ;;;                                                                  ;;;
 ;;; Original author: Scott McKay                                     ;;;
 ;;;                                                                  ;;;
 
 ;;; Protobuf serialization from Lisp objects
 
+;;; Size caches
+
+(defgeneric make-size-cache (object type)
+  (:documentation
+   "Make an object size cache for 'object'."))
+
+;; Note that this gets called on the top-level object being serialized
+;; This means that either all the objects in the tree should be subclasses
+;; of 'base-protobuf-message', or none of them should be. If the root is
+;; not a 'base-protobuf-message', then things will work but be slower; if
+;; the root is a 'base-protobuf-message', but some children are not, then
+;; serialization will fail.
+(defmethod make-size-cache ((object standard-object) type)
+  (declare (ignore type))
+  ;; No '%cached-size' slot in standard objects, create a "visited" table
+  (make-hash-table))
+
+(defmethod make-size-cache ((object base-protobuf-message) type)
+  ;; In classes defined by Protobufs, we will use the '%cached-size' slot
+  ;; This clears the size cache for the tree of objects
+  (clear-size-cache object type)
+  nil)
+
+
+(declaim (inline cached-object-size))
+(defun cached-object-size (object visited)
+  (declare #.$optimize-fast-unsafe)
+  (if visited
+    (gethash object visited)
+    ;; Warning, Will Robinson! Danger!
+    ;; If there's no 'visited' table, assume there's a cached size slot.
+    ;; We should use methods speciaized on 'base-protobuf-message',
+    ;; but we're trying to max out the speed.
+    (slot-value object '%cached-size)))
+
+(defun (setf cached-object-size) (size object visited)
+  (declare #.$optimize-fast-unsafe)
+  (if visited
+    (setf (gethash object visited) size)
+    (setf (slot-value object '%cached-size) size)))
+
+
+(defgeneric clear-size-cache (object type)
+  (:documentation
+   "Clear the size cache for a tree of objects."))
+
+(defmethod clear-size-cache ((object standard-object) type)
+  (declare (ignore type))
+  nil)
+
+(defmethod clear-size-cache ((object base-protobuf-message) type)
+  (let ((message (find-message-for-class type)))
+    (assert message ()
+            "There is no Protobuf message having the type ~S" type)
+    (macrolet ((read-slot (object slot reader)
+                 `(if ,reader
+                    (funcall ,reader ,object)
+                    (slot-value ,object ,slot))))
+      (labels ((do-field (object trace field)
+                 (let* ((type   (if (eq (proto-class field) 'boolean) :bool (proto-class field)))
+                        (slot   (proto-value field))
+                        (reader (proto-reader field))
+                        msg)
+                   (when (or slot reader)
+                     (cond ((eq (proto-required field) :repeated)
+                            (cond ((or (and (proto-packed field) (packed-type-p type))
+                                       (keywordp type)))
+                                  ((typep (setq msg (and type (find-message trace type))) 'protobuf-message)
+                                   (setf (slot-value object '%cached-size) nil)
+                                   (doseq (v (if slot (read-slot object slot reader) (list object)))
+                                     (dolist (f (proto-fields msg))
+                                       (do-field v msg f))))))
+                           (t
+                            (cond ((keywordp type))
+                                  ((typep (setq msg (and type (find-message trace type))) 'protobuf-message)
+                                   (setf (slot-value object '%cached-size) nil)
+                                   (let ((v (if slot (read-slot object slot reader) object)))
+                                     (when v
+                                       (dolist (f (proto-fields msg))
+                                         (do-field v msg f))))))))))))
+          (declare (dynamic-extent #'do-field))
+          (setf (slot-value object '%cached-size) nil)
+          (dolist (field (proto-fields message))
+            (do-field object message field))
+          nil))))
+
+
 ;;; Serialization
 
 (defun serialize-object-to-file (filename object type &key visited)
    'type' is the Lisp name of a Protobufs message (usually the name of a 
    Lisp class) or a 'protobuf-message'.
    'visited' is a hash table used to cache object sizes. If it is supplied, it will be
-   cleared before it is used; otherwise, a fresh table will be created.
+   cleared before it is used; otherwise, a fresh table will be created if necessary.
    The return value is the buffer containing the serialized object. If the stream is
    nil, the buffer is not actually written to anywhere."
-  (let* ((visited (let ((v (or visited (make-hash-table))))
-                    (clrhash v)
+  (let* ((visited (let ((v (or visited (make-size-cache object type))))
+                    (when v (clrhash v))
                     v))
+         ;; Use 'object-size' to forcibly recompute all the sizes
          (size    (object-size object type visited))
          (buffer  (make-byte-vector size)))
     (serialize-object object type buffer 0 visited)
     Lisp class) or a 'protobuf-message'.
     The object is serialized into the byte array given by 'buffer' starting
     at the fixnum index 'index' using the wire format.
-    'visited' is a hash table used to cache object sizes.
+    'visited' is a hash table used to cache object sizes; if this is nil, then
+    the object caches its size itself in a '%cached-size' slot.
     The return value is the buffer containing the serialized object."))
 
 (defmethod serialize-object (object type buffer &optional start visited)
     (serialize-object object message buffer start visited)))
 
 ;; 'visited' is used to cache object sizes
-;; If it's passed in explicitly, it is assumed to already have the sizes within it
+;; If it's non-nil. it must to be a table with the sizes already in it
+;; If it's nil, then the objects must have a '%cached-size' slot
 ;; The default method uses metadata from the protobuf "schema" for the message
 (defmethod serialize-object (object (message protobuf-message) buffer &optional start visited)
   (declare (type (simple-array (unsigned-byte 8)) buffer))
-  (let ((visited (or visited (make-hash-table)))
-        (index   (or start 0)))
+  (let ((index (or start 0)))
     (declare (type fixnum index))
     (macrolet ((read-slot (object slot reader)
                  ;; Don't do a boundp check, we assume the object is fully populated
                                        ;; To serialize an embedded message, first say that it's
                                        ;; a string, then encode its size, then serialize its fields
                                        (let ((tag (make-tag $wire-type-string (proto-index field)))
-                                             (len (object-size v msg visited)))
+                                             (len (cached-object-size v visited)))
                                          (setq index (encode-uint32 tag buffer index))
                                          (setq index (encode-uint32 len buffer index)))
                                        (dolist (f (proto-fields msg))
                                              (do-field v msg f))
                                            (setq index (encode-uint32 tag2 buffer index)))
                                          (let ((tag (make-tag $wire-type-string (proto-index field)))
-                                               (len (object-size v msg visited)))
+                                               (len (cached-object-size v visited)))
                                            (setq index (encode-uint32 tag buffer index))
                                            (setq index (encode-uint32 len buffer index))
                                            (dolist (f (proto-fields msg))
    "Computes the size in bytes of the object 'object' of type 'type'.
     'type' is the Lisp name of a Protobufs message (usually the name of a 
     Lisp class) or a 'protobuf-message'.
-    'visited' is a hash table used to cache object sizes.
+    'visited' is either a hash table used to cache object sizes,
+    or is nil, in which case the objects must have a '%cached-size' slot in them.
     The return value is the size of the object in bytes."))
 
 (defmethod object-size (object type &optional visited)
 ;; 'visited' is used to cache object sizes
 ;; The default method uses metadata from the protobuf "schema" for the message
 (defmethod object-size (object (message protobuf-message) &optional visited)
-  (let ((size (and visited (gethash object visited))))
-    (when size
-      (return-from object-size size)))
   (let ((size 0))
     (declare (type fixnum size))
     (macrolet ((read-slot (object slot reader)
                                          (iincf size (length32 tag2))))
                                      (doseq (v (if slot (read-slot object slot reader) (list object)))
                                        (let ((tag (make-tag $wire-type-string (proto-index field)))
-                                             (len (object-size v msg visited)))
+                                             (len (or (cached-object-size v visited)
+                                                      (object-size v msg visited))))
                                          (iincf size (length32 tag))
                                          (iincf size (length32 len))
                                          (dolist (f (proto-fields msg))
                                      (when v
                                        (if (eq (proto-message-type msg) :group)
                                          (let ((tag1 (make-tag $wire-type-start-group (proto-index field)))
-                                             (tag2 (make-tag $wire-type-end-group   (proto-index field))))
+                                               (tag2 (make-tag $wire-type-end-group   (proto-index field))))
                                            (iincf size (length32 tag1))
                                            (dolist (f (proto-fields msg))
                                              (do-field v msg f))
                                            (iincf size (length32 tag2)))
                                          (let ((tag (make-tag $wire-type-string (proto-index field)))
-                                               (len (object-size v msg visited)))
+                                               (len (or (cached-object-size v visited)
+                                                        (object-size v msg visited))))
                                            (iincf size (length32 tag))
                                            (iincf size (length32 len))
                                            (dolist (f (proto-fields msg))
         (declare (dynamic-extent #'do-field))
         (dolist (field (proto-fields message))
           (do-field object message field))
-        (when visited
-          (setf (gethash object visited) size))   ;cache the size
+        (setf (cached-object-size object visited) size)          ;cache the size
         size))))
 
 \f
                                (let ((tag1 (make-tag $wire-type-start-group index))
                                      (tag2 (make-tag $wire-type-end-group   index)))
                                  `(,iterator (,vval ,reader)
-                                    (let ((len (or (and visited (gethash ,vval visited))
-                                                   (object-size ,vval ,msg visited))))
+                                    (let ((len (cached-object-size ,vval visited)))
                                       (setq ,vidx (encode-uint32 ,tag1 ,vbuf ,vidx))
                                       (serialize-object ,vval ,msg ,vbuf ,vidx visited)
                                       (iincf ,vidx len)
                                       (setq ,vidx (encode-uint32 ,tag2 ,vbuf ,vidx)))))
                                (let ((tag (make-tag $wire-type-string index)))
                                  `(,iterator (,vval ,reader)
-                                    (let ((len (or (and visited (gethash ,vval visited))
-                                                   (object-size ,vval ,msg visited))))
+                                    (let ((len (cached-object-size ,vval visited)))
                                       (setq ,vidx (encode-uint32 ,tag ,vbuf ,vidx))
                                       (setq ,vidx (encode-uint32 len ,vbuf ,vidx))
                                       (serialize-object ,vval ,msg ,vbuf ,vidx visited)
                                    (tag2 (make-tag $wire-type-end-group   index)))
                                `(let ((,vval ,reader))
                                   (when ,vval
-                                    (let ((len (or (and visited (gethash ,vval visited))
-                                                   (object-size ,vval ,msg visited))))
+                                    (let ((len (cached-object-size ,vval visited)))
                                       (setq ,vidx (encode-uint32 ,tag1 ,vbuf ,vidx))
                                       (serialize-object ,vval ,msg ,vbuf ,vidx visited)
                                       (iincf ,vidx len)
                              (let ((tag (make-tag $wire-type-string index)))
                                `(let ((,vval ,reader))
                                   (when ,vval
-                                    (let ((len (or (and visited (gethash ,vval visited))
-                                                   (object-size ,vval ,msg visited))))
+                                    (let ((len (cached-object-size ,vval visited)))
                                       (setq ,vidx (encode-uint32 ,tag ,vbuf ,vidx))
                                       (setq ,vidx (encode-uint32 len ,vbuf ,vidx))
                                       (serialize-object ,vval ,msg ,vbuf ,vidx visited)
                                (let ((tag1 (make-tag $wire-type-start-group index))
                                      (tag2 (make-tag $wire-type-end-group   index)))
                                  `(,iterator (,vval ,reader)
-                                    (let ((len (or (and visited (gethash ,vval visited))
+                                    (let ((len (or (cached-object-size ,vval visited)
                                                    (object-size ,vval ,msg visited))))
                                       (iincf ,vsize (length32 ,tag1))
                                       (iincf ,vsize len)
                                       (iincf ,vsize ,tag2))))
                                (let ((tag (make-tag $wire-type-string index)))
                                  `(,iterator (,vval ,reader)
-                                    (let ((len (or (and visited (gethash ,vval visited))
+                                    (let ((len (or (cached-object-size ,vval visited)
                                                    (object-size ,vval ,msg visited))))
                                       (iincf ,vsize (length32 ,tag))
                                       (iincf ,vsize (length32 len))
                                    (tag2 (make-tag $wire-type-end-group   index)))
                                `(let ((,vval ,reader))
                                   (when ,vval
-                                    (let ((len (or (and visited (gethash ,vval visited))
+                                    (let ((len (or (cached-object-size ,vval visited)
                                                    (object-size ,vval ,msg visited))))
                                       (iincf ,vsize (length32 ,tag1))
                                       (iincf ,vsize len)
                              (let ((tag (make-tag $wire-type-string index)))
                                `(let ((,vval ,reader))
                                   (when ,vval
-                                    (let ((len (or (and visited (gethash ,vval visited))
+                                    (let ((len (or (cached-object-size ,vval visited)
                                                    (object-size ,vval ,msg visited))))
                                       (iincf ,vsize (length32 ,tag))
                                       (iincf ,vsize (length32 len))
            (,vobj (,vclass (eql ,message)) &optional visited)
          (declare #.$optimize-serialization)
          (declare (ignorable visited))
-         (let ((,vsize (and visited (gethash ,vobj visited))))
-           (when ,vsize
-             (return-from object-size ,vsize)))
          (let ((,vsize 0))
            (declare (type fixnum ,vsize))
            ,@sizers
-           (when visited
-             (setf (gethash ,vobj visited) ,vsize))
+           (setf (cached-object-size ,vobj visited) ,vsize)
            ,vsize)))))