github.com/cloudwego/dynamicgo@v0.2.6-0.20240519101509-707f41b6b834/proto/INTRODUCTION.md (about)

     1  # DynamicGo For Protobuf Protocol
     2  该模块实现了dynamicgo对于高性能protobuf协议泛化调用的支持,由于protobuf协议与thrift协议的编码格式和规范存在较大差异,这里我们将详细介绍dynamicgo对于protobuf协议的相关设计细节和[飞书设计文档](https://zt69na939u.feishu.cn/docx/EzzZdEFDDofpFtxjFVFcMjQgnNd)
     3  
     4  ## protobuf协议重要细节
     5  在介绍链路设计之前我们有必要先了解一下protobuf协议的规范,后续的各项设计都将在严格遵守该协议的规范的基础上进行改进。
     6  
     7  ## Protobuf源码链路
     8  Protobuf源码链路过程主要涉及三类数据类型之间的转换,以Message对象进行管理,自定义Value类实现反射和底层存储,用Message对象能有效地进行管理和迁移,但也带来了许多反射和序列化开销。
     9  
    10  ![](../image/intro-9.png)
    11  
    12  ### Protobuf编码格式
    13  protobuf编码字段格式大体上遵循TLV(Tag,Length,Value)的结构,但针对具体的类型存在一些编码差异,这里我们将较为全面的给出常见类型的编码模式。
    14  
    15  #### Message Field
    16  protobuf的接口必定包裹在一个Message类型下,因此无论是request还是response最终都会包裹成一个Message对象,那么Field则是Message的一个基本单元,任何类型的字段都遵循这样的TLV结构:
    17  ![](../image/intro-10.png)
    18  - Tag由字段编号Field_number和wiretype两部分决定,对位运算后的结果进行varint压缩,得到压缩后的字节数组作为字段的Tag。
    19  - wiretype的值则表示的是value部分的编码方式,可以帮助我们清楚如何对value的读取和写入。**到目前为止,wiretype只有VarintType,Fixed32Type,Fixed64Type,BytesType四类编码方式。** 
    20    - 其中属于VarintType类型的value会被压缩后再编码。
    21    - 属于Fixed32Type和Fixed64Typ固定长度类型的value分布占用4字节和8字节。
    22    - 属于不定长编码类型BytesType的则会编码计算value部分的ByteLen再拼接value。
    23  - ByteLen用于编码表示value部分所占的字节长度,同样我们的bytelen的值也是经过varint压缩后得到的,但bytelen并不是每个字段都会带有,只有不定长编码类型BytesType才会编码bytelen。**ByteLen由于其不定长特性,计算过程在序列化过程中是需要先预先分配空间,记录位置,等写完内部字段以后再回过头来填充。**
    24  - Value部分是根据wiretype进行对应的编码,字段的value可能存在嵌套的T(L)V结构,如字段是Message,Map等情况。
    25  #### Message
    26  关于Message本身的编码是与上面提到的字段编码一致的,也就是说遇到一个Message字段时,我们会先编码Message字段本身的Tag和bytelen,然后再来逐个编码Message里面的每个字段的TLV。**但需要提醒注意的是,在最外层不存在包裹整个Message的Tag和bytelen前缀,只有每个字段的TLV拼接。**
    27  
    28  #### List
    29  list字段比较特殊,protobuf为了节省存储空间,根据list元素的类型分别采用不同的编码模式。
    30  - Packed List Mode  
    31  如果list的元素value本身属于VarintType/Fixed32Type/Fixed64Type编码格式,那么将采用packed模式编码整个List,在这种模式下的list是有bytelen的。protobuf3默认对这些类型的list启用packed。
    32  ![](../image/intro-11.png)
    33  - UnPacked List Mode  
    34  **当list元素属于BytesType编码格式时**,list将使用unpacked模式,直接编码每一个元素的TLV,这里的V可能是嵌套的如List<Message>模式,**那么unpacked模式下所有元素的tag都是相同的,list字段的结束标志为与下一个TLV字段编码不同或者到达buf末尾。**
    35  ![](../image/intro-12.png)
    36  
    37  #### Map
    38  Map编码模式与unpacked list相同,根据官方文档所说,Map的每个KV键值对其实本质上就是一个Message,固定key的Field_number是1,value的Field_number是2,那么Map的编码模式就和`List<Message>`一致了。
    39  ![](../image/intro-13.png)
    40  
    41  
    42  
    43  ### Descriptor细节
    44  这里主要介绍一下源码的descriptor设计上的一些需要注意的细节,有助于理解我们自定义descriptor的方案。
    45  - Service接口由ServiceDescriptor来描述,ServiceDescriptor当中可以拿到每个rpc函数的MethodDescriptor。
    46  - MethodDescriptor中Input()和output()两个函数返回值均为MessageDescriptor分别表示request和response。
    47  - MessageDescriptor专门用来描述一个Message对象(也可能是一个MapEntry),可以通过Fields()找到每个字段的FieldDescriptor。
    48  - FieldDescriptor兼容所有类型的描述。
    49  
    50  ![](../image/intro-14.png)
    51  
    52  **FieldDescriptor注意要点:**
    53  1. 如果字段是一个List,那么FieldDescriptor中Kind()表示的是元素的Kind(),字段是否是List只能通过IsList()函数区分,即当字段为List<Message>的情况,那么此时调用Kind()得到的为messagekind。
    54  2. 如果字段是Map,那么FieldDescriptor中Kind()表示为messageKind,字段是否是List只能通过IsMap()函数区分。
    55  3. 当字段为messagekind时(可能的情况message,List<Message>,map<int/string,Message>),可以通过Message()方法,可以获得下层嵌套的MessageDescriptor。
    56  4. MapKey的FieldDescriptor被限定为uint/bool/int/string类型。目前的代码设计上将uint32,uint64,int32,int64全都转成了int,bool没处理。
    57  
    58  ## Descriptor自定义设计
    59  Descriptor的设计原理基本与源码一致,不过为了兼容性和避免强转,这里我们通thrift抽象了一个TypeDescriptor来处理类型问题,但因为protobuf编码格式的问题,TypeDescriptor做了一部分修改。
    60  
    61  parse过程主要利用的是[`github.com/jhump/protoreflect@v1.8.2`](https://pkg.go.dev/github.com/jhump/protoreflect@v1.8.2)解析得到的第三方descriptor进行的再封装,从实现过程上来看,与高版本protoreflect利用[`protocompile`](https://pkg.go.dev/github.com/bufbuild/protocompile)对原始链路再make出源码的warp版本一致,更好的实现或许是处理利用protoreflect中的ast语法树构造。
    62  
    63  ### TypeDescriptor
    64  ```go
    65  type TypeDescriptor struct {
    66  	baseId FieldNumber // for LIST/MAP to write field tag by baseId
    67  	typ    Type
    68  	name   string
    69  	key    *TypeDescriptor
    70  	elem   *TypeDescriptor
    71  	msg    *MessageDescriptor // for message, list+message element and map key-value entry
    72  }
    73  ```
    74  
    75  - `baseId`:因为对于LIST/MAP类型的编码特殊性,如在unpack模式下,每一个元素都需要编写Tag,我们必须在构造时针对LIST/MAP提供fieldnumber,来保证读取和写入的自反射性。
    76  - `msg`:这里的msg不是仅Message类型独有,主要是方便J2P部分对于List<Message>和marshalto中map获取可能存在value内部字段缺失的MapEntry的MassageDescriptor(在源码的设计理念当中MAP的元素被认为是一个含有key和value两个字段的message)的时候能利用TypeDescriptor进入下一层嵌套。具体位置:[j2p for List<Message>](../conv/j2p/decode.go#L422)和[map entry marshalto](./generic/value.go#L853)。
    77  - `typ`:这里的Type不同于源码的FieldDescriptor,对LIST/MAP做了单独定义,对应`type.go`文件下的[`Type`](./type.go#L87)。
    78  
    79  ### FieldDescriptor
    80  ```go
    81  type FieldDescriptor struct {
    82  	kind     ProtoKind // the same value with protobuf descriptor
    83  	id       FieldNumber
    84  	name     string
    85  	jsonName string
    86  	typ      *TypeDescriptor
    87  }
    88  ```
    89  
    90  - FieldDescriptor的设置是希望变量和函数作用与源码FieldDescriptor基本一致,只是增加`*TypeDescriptor`可以更细粒度的反应类型以及对FieldDescriptor的API实现。
    91  - `kind`:保留kind,对应`type.go`文件下的[`ProtoKind`](./type.go#L41),LIST情况下,kind是列表元素的kind类型,MAP和MESSAGE情况下,messagekind。
    92  
    93  ## ProtoBuf DOM设计
    94  显然protobuf的层层包裹设计与thrift差异很大,这也给DOM设计造成了许多的困难,虽然思想上和dynamicgo-thrift一致,但为了更统一化的处理节点和片段管理,这里我们对链路的存储格式和函数设计做了一些改进。
    95  ### 存储结构
    96  #### Node结构
    97  ```go
    98  type Node struct {
    99      t    proto.Type
   100      et   proto.Type
   101      kt   proto.Type
   102      v    unsafe.Pointer
   103      l    int // ptr len
   104      size int // only for MAP/LIST element counts
   105  }
   106  ```
   107  
   108  **具体的存储规则如下:**
   109  - 基本类型Node表示:指针v的起始位置不包含tag,指向(L)V,t = 具体类型,et,kt = UNKNOWN
   110  - Message类型Node:指针v的起始位置不包含tag,指向(L)V,如果是root节点,那么v的起始位置本身没有前缀的L,直接指向了V即第一个字段的tag上,而其余子结构体都包含前缀L。
   111    - t = MESSAGE,et,kt = UNKNOWN
   112  - List类型Node:为了兼容List的两种模式和自反射的完整性,我们必须包含list的完整片段和tag。**因此List类型的节点,v指向list的tag上,即如果是packed模式就是list的tag上,如果是unpacked则在第一个元素的tag上。** 
   113    - t = LIST,et = 元素类型,kt = UNKNOWN,size = 元素个数。
   114  - Map类型Node:同理,Map的指针v也指向了第一个pair的tag位置。
   115    - t = MAP ,kt = Key类型 et = Value类型,size = 元素个数。
   116  - UNKNOWN类型Node:无法解析的合理字段Node会将TLV完整存储,多个相同字段Id的会存到同一节点,缺点是内部的子节点无法构建,同官方源码unknownFields原理一致。
   117  - ERROR类型Node:基本与thrift一致,在setnotfound中,若片段设计与上述规则一致则可正确构造插入。
   118  - 虽然MAP/LIST的父级Node存储有些变化,但是其子元素节点都是基本类型/MESSAGE,所以**DOM当中的所有叶子节点存储格式还是基本的(L)V。**
   119  
   120  #### Value结构
   121  value的结构本身是对Node的封装,将Node与相应的descriptor封装起来,但不同于thrift,在protobuf当中由于片段无法完全自解析出具体类型,之后的涉及到具体编码的部分操作不能脱离descriptor,只能实现Value类作为调用单位。
   122  
   123  ```go
   124  type Value struct {
   125      Node
   126      Desc    *proto.TypeDescriptor
   127      IsRoot  bool
   128  }
   129  
   130  func NewRootValue(desc *proto.TypeDescriptor, src []byte) Value {
   131      return Value{
   132          Node:     NewNode(proto.MESSAGE, src),
   133          Desc: desc,
   134          IsRoot: true,
   135      }
   136  }
   137  
   138  // only for basic Node
   139  func NewValue(desc *proto.TypeDescriptor, src []byte) Value {
   140      return Value{
   141          Node: NewNode(desc.Type(), src),
   142          Desc: desc,
   143      }
   144  }
   145  ```
   146  由于从rpc接口解析后我们直接得到了对应的TypeDescriptor,再加上root节点本身没有前缀TL的独特编码结构,我们设计`IsRoot`来区分root节点和其余节点,也能避免利用父类强转。
   147  
   148  #### Path与PathNode
   149  结构功能均与thrift一致,且protobuf不存在PathBinKey。
   150  ```go
   151  // Path represents the relative position of a sub node in a complex parent node
   152  type Path struct {
   153      t PathType // 类似div标签的类型,用来区分field,map,list元素,帮助判定父级嵌套属于什么类型结构
   154      
   155      v unsafe.Pointer // PathStrKey, PathFieldName类型,存储的是Key/FieldName的字符串指针
   156      
   157      l int 
   158      // PathIndex类型,表示LIST的下标
   159      // PathIntKey类型,表示MAPKey的数值
   160      // PathFieldId类型,表示字段的id
   161  }
   162  
   163  pathes []Path : 合理正确的Path数组,可以定位到嵌套复杂类型里具体的key/index的位置
   164  ```
   165  ### 构建DOM Tree
   166  #### PathNode.Load/Value.Children
   167  构建DOM支持懒加载和全加载即recurse参数。
   168  - 在懒加载模式下LIST/MAP的Node当中size不会同步计算,因为字段会直接skip,而全加载在构造叶子节点的同时顺便更新了size,但不影响后续的迭代器反射,后续迭代器翻译还是以Node片段长度为准,而不是直接看size。
   169  - LIST/MAP的节点包含tag,代码中hindlechild函数会传入一个tagL参数,遇到LIST/MAP时取地址片段时会让start前移tag长度。
   170  查找字段
   171  
   172  ### 查找字段
   173  #### GetByPath/GetByPathWithAddress/GetMany
   174  支持任意Node查找,查找函数设计了三个外部API:GetByPath,GetByPathWithAddress,GetMany。
   175  - GetByPath函数:返回查找出来的Value,查找失败返回ERRORNode
   176  - GetByPathWithAddress:返回Value和当前调用节点到查找节点过程中每个Path嵌套层的tag位置的偏移量。[]address与[]Path个数对应,若调用节点为root节点,那么都是到buf首地址的偏移量。、
   177  - GetMany:传入当前嵌套层下合理的[]PathNode的Path部分,直接返回构造出来多个Node,得到完整的[]PathNode,可继续用于MarshalMany
   178  
   179  #### 设计思路
   180  查找过程是根据传入的[]Path来循环遍历查找每一层Path嵌套里面对应的位置,根据嵌套的Path类型(fieldId,mapKey,listIndex),调用不同的search函数。
   181  - searchFieldId:找到字段,返回字段tag位置,未找到则返回message片段末尾。
   182  - searchInt/strKey:找到字段,返回字段tag位置,未找到则返回map片段末尾。
   183  - searchIndex:找到字段,若为pack则返回value起始位置,若为unpack返回元素tag位置,未找到则返回list片段末尾。
   184  构造最终返回的Node时,根据具体类型看是否需要去除tag即可,返回的[]address刚好在search过程中完成了append每一层Path的tag偏移量。
   185  
   186  ### 插入字段
   187  #### SetByPath/setMany
   188  两个外部API:SetByPath和SetMany。插入字段思路大体与thrift一致,**区别在于:为了保证兼容pack/unpack/map/message的统一性,不同于thrift,对于新增数据只能采用尾插法,完成插入操作后需要进行回写嵌套层bytelen的操作。**
   189  - SetByPath:只支持root节点调用,保证UpdateByLen更新的完整和正确性。
   190  - setMany:可支持局部节点插入,除了构造当前嵌套层下合理的PathNode,还需要额外传入rootValue的指针,目标插入节点到root节点的位置,以及到root的[]Path,完成多个节点插入后。判断当前调用节点是否为root,若不是则完成当前层的updatebytelen后,需要利用rootValue再调用一次updatebytelen。
   191  
   192  #### Updatebytelen细节
   193  - 计算插入完成后新的长度与原长度的差值,存下当前片段增加或者减少的diffLen。
   194  - 从里向外逐步更新[]Path数组中存在bytelen的嵌套层(只有packed list和message)的bytelen字节数组。
   195  - 更新规则:先readTag,然后再readLength,得到解varint压缩后的具体bytelen数值,计算newlen = bytelen + diffLen,计算newbytelen压缩后的字节长度与原bytelen长度的差值sublen,并累计diffLen+=sublen。
   196  - 指针向前移动到下一个path和address。
   197  
   198  ![](../image/intro-15.png)
   199  
   200  ### 删除字段
   201  #### UnsetByPath
   202  目前只支持root节点调用。删除字段同插入字段,即找到对应的片段后直接将片段置空,然后更新updatebytelen。
   203  
   204  ### PathNode序列化
   205  对外API:Marshal和MarshalTo。
   206  #### Marshal
   207  DOM建好PathNode后,可直接调用PathNode的Marshal方法即可,本质是BFS遍历拼接DOM的所有叶子节点,Tag部分会通过Path类型和Node类型进行补全,bytelen会预先分配,根据实际遍历节点进行回写,特殊处理root层,不需要写message的TL前缀。
   208  
   209  ![](../image/intro-16.png)
   210  
   211  #### MarshalTo
   212  MarshalTo主要用于裁剪字段,获取新旧descriptor中共有的字段id,然后进行翻译,可支持多层嵌套内部的字段缺失,LIST/MAP内部元素字段缺失,核心关键在于`message`,`List<Message>`,`map<int/string,Message>`的descriptor均为messageKind。
   213  
   214  ## JSON协议转换
   215  ### ProtoBuf——>JSON
   216  #### 协议转换过程
   217  协议转换的过程可以理解为逐字节解析 ProtoBuf,并结合 Descriptor 类型编码为 JSON 到输出字节流,整个过程是 in-place 进行的,并且结合内存池技术,仅需为输出字节流分配一次内存即可。
   218  ProtoBuf——>JSON 的转换过程如下:
   219  1. 根据输入的 Descriptor 指针类型区分,若为 Singular(string/number/bool/Enum) 类型,跳转到
   220  2. 按照 Message([Tag] [Length] [TLV][TLV][TLV]....)编码格式对输入字节流执行 varint解码,将Tag解析为 fieldId(字段ID)、wireType(字段wiretype类型);
   221  3. 根据第2步解析的 fieldId 确定字段 FieldDescriptor,并编码字段名 key 作为 jsonKey 到输出字节流;
   222  4. 根据 FieldDescriptor 确定字段类型(Singular/Message/List/Map),选择不同编码方法编码 jsonValue 到输出字节流;
   223  5. 如果是 Singular 类型,直接编码到输出字节流;
   224  6. 其它类型递归处理内部元素,确定子元素 Singular 类型进行编码,写入输出字节流中;
   225  7. 及时输入字节流读取位置和输出字节流写入位置,跳回2循环处理,直到读完输入字节流。
   226  
   227  #### List类型处理
   228  > List类型包括 PackedList、UnpackedList,区别为元数据类型是否为 Number。在转换时根据 FieldDescriptor 区分这两种类型;
   229  - PackedList:[Tag][Length][Value Value Value Value ....]
   230       解析完 Tag、Length后循环处理 Value数组,每解析一个 Value元素,编码到输出字节流;
   231  - UnpackedList:[Tag][Length][Value] [Tag][Length][Value] ... 
   232    所有子节点共享 fieldId(字段ID),因此每次预解析 Tag 得到 elementNumber,并以 (elementNumber==fieldId)=true 作为循环条件直到结束。
   233  
   234  #### Map类型处理
   235  Map:[PairTag][PairLength][KeyTag][KeyLength][KeyValue][ValueTag][ValueLength][ValueValue]....
   236  
   237  Map 类型所有 PairTag 共享相同 fieldId(字段ID)。因此预解析 PairTag 得到 pairNumber,并以 (pairNumber==fieldId)=true 作为循环条件直到结束。
   238  
   239  ### JSON——>ProtoBuf
   240  协议转换过程中通过 [sonic](https://github.com/bytedance/sonic) 框架实现 in-place 遍历 JSON,并实现了接口 Onxxx(OnBool、OnString、OnInt64....)方法达到编码 ProtoBuf 的目标,编码过程也做到了 in-place。
   241  
   242  #### VisitorUserNode 结构
   243  因为在编码 Protobuf 格式的 Mesage/UnpackedList/Map 类型时需要对字段总长度回写,并且在解析复杂类型(Message/Map/List)的子元素时需要依赖复杂类型 Descriptor 来获取子元素 Descriptor,所以需要 VisitorUserNode 结构来保存解析 json 时的中间数据。
   244  
   245  ```go
   246  type VisitorUserNode struct {
   247      stk             []VisitorUserNodeStack
   248      sp              uint8
   249      p               *binary.BinaryProtocol
   250      globalFieldDesc *proto.FieldDescriptor
   251  }
   252  ```
   253  1. stk:记录解析时中间变量的栈结构,在解析 Message 类型时记录 MessageDescriptor、PrefixLen;在解析 Map 类型时记录 FieldDescriptor、PairPrefixLen;在解析 List 类型时记录 FieldDescriptor、PrefixListLen;
   254  2. sp:当前所处栈的层级;
   255  3. p:输出字节流;
   256  4. globalFieldDesc:每当解析完 MessageField 的 jsonKey 值,保存该字段 Descriptor 值;
   257  
   258  #### VisitorUserNodeStack 结构
   259  记录解析时字段 Descriptor、回写长度的起始地址 PrefixLenPos 的栈结构
   260  ```go
   261  type VisitorUserNodeStack struct {
   262      typ   uint8
   263      state visitorUserNodeState
   264  }
   265  ```
   266  1. typ:当前字段的类型,取值有对象类型(objStkType)、数组类型(arrStkType)、哈希类型(mapStkType);
   267  2. state:存储详细的数据值;
   268  
   269  #### visitorUserNodeState 结构
   270  ```go
   271  type visitorUserNodeState struct {
   272      msgDesc   *proto.MessageDescriptor
   273      fieldDesc *proto.FieldDescriptor
   274      lenPos    int
   275  }
   276  ```
   277  1. msgDesc:记录 root 层的动态类型描述 MessageDescriptor;
   278  2. fieldDesc:记录父级元素(Message/Map/List)的动态类型描述 FieldDescriptor;
   279  3. lenPos:记录需要回写 PrefixLen 的位置;
   280  
   281  #### 协议转换过程
   282  JSON——>ProtoBuf 的转换过程如下:
   283  1. 从输入字节流中读取一个 json 值,并判断其具体类型(object/array/string/float/int/bool/null);
   284  2. 如果是 object 类型,可能对应 ProtoBuf MapType/MessageType,sonic 会按照 `OnObjectBegin()——>OnObjectKey()——>decodeValue()...` 顺序处理输入字节流。
   285        - OnObjectBegin()阶段解析具体的动态类型描述 FieldDescriptor 并压栈;
   286        - OnObjectKey() 阶段解析 jsonKey 并以 ProtoBuf 格式编码 Tag、Length 到输出字节流;
   287        - decodeValue()阶段递归解析子元素并以 ProtoBuf 格式编码 Value 部分到输出字节流,若子类型为复杂类型(Message/Map),会递归执行第 2 步;若子类型为复杂类型(List),会递归执行第 3 步。
   288  3. 如果是 array 类型,对应 ProtoBuf PackedList/UnpackedList,sonic 会按照 `OnObjectBegin()——>OnObjectKey()——>OnArrayBegin()——>decodeValue()——>OnArrayEnd()...` 顺序处理输入字节流。
   289       - `OnObjectBegin()`阶段处理解析 List 字段对应动态类型描述 FieldDescriptor 并压栈;
   290       - `OnObjectKey()`阶段解析 List 下子元素的动态类型描述 FieldDescriptor 并压栈;
   291       - `OnArrayBegin()`阶段将 PackedList 类型的 Tag、Length 编码到输出字节流;
   292       - `decodeValue()`阶段循环处理子元素,按照子元素类型编码到输出流,若子元素为复杂类型(Message),会跳转到第 2 步递归执行。
   293  4. 在结束处理某字段数据后执行 `onValueEnd()、OnArrayEnd()、OnObjectEnd()`,获取栈顶 lenPos 数据,对字段长度部分回写并退栈。
   294  5. 更新输入和输出字节流位置,跳回第 1 步循环处理,直到处理完输入流数据。
   295  
   296  ## 性能测试
   297  构造与thrift性能测试基本相同的[baseline.proto](../testdata/idl/baseline.proto)文件,定义了对应的简单([Small](../testdata/idl/baseline.proto#L7))、复杂([Medium](../testdata/idl/baseline.proto#L22))、简单缺失([SmallPartial](../testdata/idl/baseline.proto#L16))、复杂缺失([MediumPartial](../testdata/idl/baseline.proto#L40)) 两个对应子集,并用kitex命令生成了对应的[baseline.pb.go](../testdata/kitex_gen/pb/baseline/baseline.pb.go)。
   298  主要与Protobuf-Go官方源码进行比较,部分测试与kitex-fast也进行了比较,测试环境如下:
   299  - OS:Windows 11 Pro Version 23H2
   300  - GOARCH: amd64
   301  - CPU: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
   302  - Go VERSION:1.20.5
   303  
   304  ### 反射
   305  - 代码:[dynamicgo/testdata/baseline_pg_test.go](../testdata/baseline_pg_test.go)
   306  - 图中列举了DOM常用操作的性能,测试细节与thrift相同。
   307  - MarshalTo方法:相比protobufGo提升随着数据规模的增大趋势越明显,ns/op开销约为源码方法的0.29 ~ 0.32。
   308    
   309  ![](../image/intro-17.png)
   310  
   311  ### 字段Get/Set定量测试
   312  - 代码:[dynamicgo/testdata/baseline_pg_test.go#BenchmarkRationGet_DynamicGo](../testdata/baseline_pg_test.go#L1607)
   313  - [factor](../testdata/baseline_pg_test.go#L1563)用于修改从上到下扫描proto文件字段获取比率。
   314  - 定量测试比较方法是protobufGo的dynamicpb模块和DynamicGo的Get/SetByPath,SetMany,测试对象是medium data的情况。
   315  - Set/Get字段定量测试结果均优于ProtobufGo,且在获取字段越稀疏的情况下性能加速越明显,因为protobuf源码不论获取多少比率的字段,都需要完整序列化全体对象,而dynamicgo则是直接解析buf完成copy。
   316  - setmany性能加速更明显,在100%字段下ns/op开销约为0.11。
   317  
   318  ![](../image/intro-18.png)
   319  
   320  ### 序列化/反序列化
   321  - 代码:[dynamicgo/testdata/baseline_pg_test.go#BenchmarkProtoMarshalAll_DynamicGo](../testdata/baseline_pg_test.go#L1212)
   322  - 序列化在small规模略高于protobufGo,medium规模的数据上性能优势更明显,ns/op开销约为源码的0.54 ~ 0.84。
   323  - 反序列化在reuse模式下,small规模略高于protobufGo,在medium规模数据上性能优势更明显,ns/op开销约为源码的0.44 ~ 0.47,随数据规模增大性能优势增加。
   324  ![](../image/intro-19.png)
   325  ![](../image/intro-20.png)
   326  
   327  ### 协议转换
   328  - Json2Protobuf优于ProtobufGo,ns/op性能开销约为源码的0.21 ~ 0.89,随着数据量规模增大优势增加。
   329  - 代码:[dynamicgo/testdata/baseline_j2p_test.go](../testdata/baseline_j2p_test.go)
   330  - Protobuf2Json性能明显优于ProtobufGo,ns/op开销约为源码的0.13 ~ 0.21,而相比Kitex,ns/op约为Sonic+Kitex的0.40 ~ 0.92,随着数据量规模增大优势增加。
   331  - 代码:[dynamicgo/testdata/baseline_p2j_test.go](../testdata/baseline_p2j_test.go)
   332    
   333  ![](../image/intro-21.png)
   334  
   335  详细数据结果文件可在飞书文档中下载。
   336  
   337  ## TODO
   338  - [ ] 添加JSON协议的HttpMapping。
   339  - [ ] 修改为TypeDescriptor后,J2P由于sonic限定接口`Onxxx`,为了write tag,暂时还是只能用fielddescriptor,兼容其他类型的建议不放到snoic当中,而是走其他分支直接用binaryprotocol写入比较好。
   340  - [ ] 部分JSON option暂未实现。
   341  - [ ] DOM tree的Assign函数还需要实现,尝试实现但存在一些问题由于时间关系不太好解决。
   342  - [ ] 序列化/反序列化在small数据规模下的开销进一步优化。
   343  - [ ] 是否可以优化到不区分DOM的Node不区分root层和其他层的Message?目前的root层MESSAGE的Node不带L,而其他的都带有L,感觉可以优化之前设计没做好可能,如果统一可能有别的地方会带来些小问题,比如在非递归的DOM下marshal则需要自己补充L,因为Path只补齐了Tag。
   344  - [ ] 用Kind的地方设计如何方便地完全被TypeDescriptor替代。
   345  - [ ] DOM对于Enum,Oneof,Extension(Extension已经在protobuf3官方弃用)的支持。
   346  - [ ] DOM相关的部分option有待实现。