- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
Dart VM Service Protocol(Dart 服务协议)是 Dart SDK 中 VM 与调试器、性能分析器等外部工具通信的标准接口。本指南基于 runtime/vm/service/service_evolution.md 整理,完整呈现 Dart Service Protocol 设计者与维护者在扩展协议时的评审准则与心智模型,并结合 runtime/vm/service/service.md、runtime/vm/service.cc 等仓库源码佐证这些原则在真实实现中的落地方式。读完本文,你将掌握:什么样的 RPC 提案会被协议维护者接受、private RPC 的边界在哪里、实验性功能如何安全地进入协议,以及如何围绕“无副作用、实现无关、兼顾低端设备”三大原则设计可靠的协议扩展。
一、背景:Dart Service Protocol 是什么
Dart Service Protocol 是用于与正在运行的 Dart VM 通信的 JSON-RPC 2.0 协议。以--observe标志启动 VM 后,VM 会启动一个 WebSocket 服务器来服务协议请求(详见 service.md),当前仓库版本描述为 Protocol 4.22。
该协议是调试工具(如 Observatory、Dart DevTools、DDS)、性能剖析工具和 IDE 集成的基石。正因为其生态庞大、调用方众多,协议维护者必须在“快速满足新需求”与“长期保持接口稳定、简洁、可预测”之间取得平衡。service_evolution.md正是这一平衡的书面化表达:任何对协议的扩展提案,都会被放在下面描述的原则下逐一评审,通过后才能被写入协议规范。
关联文档:与协议的“静态规范”不同,service_evolution.md 描述的是协议的“演进哲学”。若需要查阅协议本身完整的 RPC、事件与类型定义,可阅读 runtime/vm/service/service.md;若关注通过核心库暴露、但独立于主协议之外的扩展 RPC,可阅读 runtime/vm/service/service_extension.md(例如
ext.dart.io.getSocketProfile这类 dart:io 扩展)。
二、新增 RPC 的硬性要求:四条评审原则
任何对 Dart service protocol 的补充都必须遵守下述原则,它们是提案能否进入协议规范的唯一标准。
2.1 只执行简单操作(Perform Only Simple Operations)
协议 API 表面应当保持尽可能小且简单。一个提案只有在满足以下任一条件时才会被接受:
- 现有 RPC 组合无法实现该行为:没有已有的 RPC 组合能够达到提案所描述的效果;
- 需要现有 RPC 无法提供的原子性:操作要求一种现有 RPC 无法达到的原子性级别;
- 能带来显著、可证明的性能提升:例如为现有 RPC 提供批量(batch)版本。
这一原则的潜台词是:在提 RPC 之前,先自查“用现有接口能否拼出这个能力”。如果答案是肯定的,维护者通常会拒绝新增接口,以避免 API 表面膨胀。
2.2 避免引入副作用(Avoid Introducing Side Effects)
一般情况下,与服务协议的交互不应改变 Dart 实例的状态。由服务协议请求引发的副作用可能极难调试——尤其是当它们改变了正在运行的 Dart 程序的状态时。虽然特殊情况下可以破例,但服务协议 API 应当:
- 无状态(Stateless):请求之间彼此独立。例如,假设执行已暂停、Dart 实例处于空闲状态,那么连续调用同一个 RPC 两次应当返回相同的结果;
- 可预测(Predictable):如果 API 必须改变状态,所产生的状态变化应当是显而易见且符合预期的。
文档中给出的正面例子是SetFlagRPC:它允许修改 Dart 实例中的特定设置,但不会引入额外的副作用。我们可以在源码中看到这一承诺的具体实现:在 service.cc 的SetFlag实现里,VM 维护了一个白名单kAllowedFlags,仅允许运行时修改pause_isolates_on_start、pause_isolates_on_exit、pause_isolates_on_unhandled_exceptions、profile_period、profiler五个标志;其余标志一律返回"Cannot set flag: cannot change at runtime"错误。源码中的注释也直接呼应了这条原则:
“Changing most flags at runtime is dangerous because e.g., it may leave the behavior generated code and the runtime out of sync.”
也就是说,即便setFlag本质上是一个“改变状态”的 RPC,它也把可变更的状态限制在最小、最可控、最符合预期的集合内——这正是“可预测”原则的工程化表达。
2.3 时刻考虑低端设备(Keep Low-Powered Devices in Mind)
Dart 实例可能运行在远程设备或低性能设备上,因此服务协议只应提供不依赖特定硬件配置的功能;同时,处理工作应尽可能放在客户端完成,以保证 Dart 实例运行在远程或低性能设备上时仍有合理的性能表现。
基于此,协议变更还必须满足两点:
- 变更必须对所有类型的设备都有效,无论其性能如何。文档给出的反面例子是:若有人请求把 profiler 的最小采样周期从 50us 降低到 10us,该提案会被拒绝——因为低端 ARM 设备无法处理 10us 的采样周期,往往会导致崩溃。这条规则在源码中有据可查:在 profiler.cc 中,
profile_period标志的默认值是 1000 微秒,且 NormalizeConfig 强制period_us = Utils::Maximum(kMinimumProfilePeriodUs, config.period_us),其中kMinimumProfilePeriodUs = 50——即便客户端传入更小的值,实现也会被钳制到 50us 的下限,从机制上杜绝低端设备被过高采样频率拖垮。 - 尽可能把处理放在客户端,以避免低端设备变慢。文档给出的例子是:在 Observatory 中,堆快照的支配树(dominator)分析是在客户端完成的。
2.4 实现无关(Implementation Agnostic)
Dart service protocol 的设计目标是:任何 Dart 实现都能使用它,而不仅仅是官方 SDK 中的 Dart VM。因此,协议必须避免通过其接口暴露底层实现细节。任何实现细节的暴露,都只能通过 private RPC 及其响应进行,且只能在该实现自身的上下文中使用(例如:Dart VM 提供的 private RPC 可以在 Observatory 中使用,但不会向外部客户端宣传,外部客户端也不应使用它们)。
文档给出了一个极具洞察力的例子:Dart VM 目前使用分代式垃圾收集器(generational GC),但这在未来未必仍然成立——所以包含 new/old space(新生代/老生代)信息的响应不应通过协议暴露。
换句话说:协议描述的是“能力”而非“实现”。一旦接口绑定了某个具体实现(比如 GC 分代、某特定 JIT 策略),未来实现变更时协议就无法演进,所有下游客户端都会被迫同步修改。
三、Private RPC:被允许的“后门”与它的边界
上述“实现无关”原则有一个明确的例外通道:private RPC。
在源码中,private RPC 的命名约定是一目了然的——方法名以下划线_开头。浏览 service.cc 的service_methods_注册表可以看到大量这类条目,例如_getAllocationProfile、_getHeapMap、_getImplementationFields、_getIsolateObjectStore、_getPorts、_getTagProfile、_reloadKernel、_collectAllGarbage、_readNativeMemory等。它们与同名或近似名目的公开 RPC(如getAllocationProfile、getPorts)并存,形成“公开能力 + 私有诊断”的双层结构。
从注册表还能观察到一条有趣的实践:_getPorts与公开的getPorts是两个独立的方法——GetPortsPrivate 使用了一套独立的参数表get_ports_private_params(见 service.cc),这正好印证了文档中“即使要暴露某个私有能力,也倾向于用新的公开 RPC 来封装,而不是把私有实现细节直接摊开”的思路。
Private RPC 的核心定位是:仅供 VM 工程师在 Observatory 等内部工具中使用,用于暴露 VM 内部实现细节。正因如此,它们的实现和响应可以在不警告的情况下随时变更,未经授权的 private RPC 请求未来也可能被直接丢弃。
四、FAQ:维护者视角下的常见问题
service_evolution.md以 FAQ 形式沉淀了维护者面对的最典型诉求与答复,以下是完整解读。
Q1:“我想获取系统 X 的信息,但服务协议没有查询接口,能加一个新 RPC 吗?”
A:也许可以。如果当前服务协议确实无法实现该操作,那么为它添加支持是可以调研的。但只有不违反上述原则的 RPC 才会被接受到协议中。
Q2:“我经常调用一串 RPC,能把它们合并成一个 RPC 吗?”
A:不可以,除非满足两个条件之一:
- 这一串 RPC 所执行的操作需要被原子地执行(需要原子性);
- 或者把工作合并进单个 RPC 能带来显著的性能收益。
这正是“只执行简单操作”原则在批量场景下的具体裁决:合并没有错,但合并不该成为掩盖接口膨胀的借口。文档同时提到,批量版 RPC(batch version of an existing RPC)是性能收益的典型合法用例。
Q3:“我想做一个需要改动服务协议的实验,该怎么做?”
A:可以,但有严格的条件。假设实验性 RPC 不会带来显著的性能影响,那么实验性功能是可以加入服务协议的,但必须满足:
- 要么被标记为private;
- 要么在文档中明确注明该功能是实验性的、可能在无通知的情况下被废弃(deprecated without notice);
- 实验性 RPC 应当生命周期很短,并在文档中标注预期的过期日期(expected expiration date)。
任何打算转正的实验性变更,都必须遵守本文第一部分(四条原则)的要求。如果需要在协议中添加实验性功能,可以联系维护者邮箱dart-service-protocol@google.com;对 Dart VM 服务实现所需的改动,也可以由dart-service-protocol@google.com或dart-vm-team@google.com的成员处理。
Q4:“有个 private RPC 正好提供我需要的功能,我能直接用它吗?”
A:不能。Private RPC 及其响应不应在 Dart SDK 之外使用。这些 RPC 与响应的实现可能在毫无警告的情况下变更,未经授权的 private RPC 请求在未来可能被直接丢弃。
Q5:“能把一个 private RPC 转正成公开 RPC 吗?”
A:一般来说,不能。虽然部分 private RPC 和响应在不太费力的情况下就能转正,但很多 private RPC 本身就暴露了 VM 的内部实现细节——它们是为 VM 工程师在 Observatory 中使用而存在的。如果确有强需求要暴露某个私有功能,需要定义一个新的公开 RPC,以保证协议不暴露系统的实现细节。
Q6:“我还有关于服务协议的问题,该联系谁?”
A:Dart VM 团队负责维护服务协议。当前维护者可以通过dart-service-protocol@google.com联系。
五、原则在真实协议中的落地印证
为了让上述原则更“看得见摸得着”,这里用仓库源码把三条关键原则与协议实现一一对应起来。
5.1 从方法注册表看“公开/私有”二元结构
在 service.cc 中,FindMethod通过线性扫描service_methods_数组来分派方法。该数组完整记录了这个版本协议中所有公开 RPC 与 private RPC 的注册信息,包括方法名、处理函数和参数描述(MethodParameter)。开发者可以直接把它当作“当前仓库协议实现的权威清单”来阅读:
- 公开方法如
getVersion、getVM、getIsolate、getCpuSamples、getSourceReport、invoke、setFlag等; - 私有方法如
_getHeapMap、_getObjectStore、_getPersistentHandles、_getReachableSize、_getRetainedSize、_reloadKernel、_collectAllGarbage等。
以getCpuSamples为例,其参数表(service.cc)只包含timeOriginMicros、timeExtentMicros两个时间范围参数——接口简单、语义清楚,没有任何与特定硬件或 GC 实现绑定的字段,正是“只执行简单操作”与“实现无关”的直接体现。
5.2 从setFlag看“可预测的副作用”
前文已述,SetFlag 通过白名单把可运行时修改的标志收敛到 5 个。更进一步,它对profile_period/profiler这两个与 profiler 相关的标志做了特殊处理:修改成功后调用Profiler::SetConfig({})让配置即时生效;若 VM stream 已启用,还会发送VMFlagUpdate事件通知订阅方。该事件类型在 service.md 中有明确定义,且在 service_event.cc 中被序列化为"VMFlagUpdate"。
这意味着:setFlag虽然“改变状态”,但①变更集合被严格限定、②变更即时反映在 profiler 配置上、③变更通过事件流显式广播——状态变化的每一个环节都是“显而易见且符合预期”的。
5.3 从 profiler 参数下限看“低端设备保护”
profiler.cc 的NormalizeConfig是“低端设备友好”原则的机制级保障:采样周期被钳制在 50us 下限(kMinimumProfilePeriodUs)、栈深度被限制在 2~255 之间。即便未来某个客户端提交了不合理的参数,VM 也会在配置层自动兜底,而不是把风险传导到低端硬件上。文档中“把尽可能多的处理放在客户端”的例子(Observatory 客户端执行堆快照支配树分析)也提醒我们:协议只传输原始数据,重活留给客户端。
六、实操建议:如何提交一份会被接受的协议提案
结合上述原则,若要向维护者提交服务协议扩展提案,可以遵循以下自查清单:
- 先做穷举自查:用现有公开 RPC 组合能否实现需求?若能,提案大概率被拒;
- 再问“是否需要原子性/批量性能”:如果都不是,考虑放弃新增 RPC,改用客户端组合逻辑;
- 审视副作用:新 RPC 是否会改变 Dart 实例状态?如果会,能否把状态变化收敛到白名单式的可控集合,并同步广播事件?是否保证“相同输入、相同输出”的无状态语义?
- 审视实现耦合:响应字段是否包含 GC 分代、代码生成策略等未来可能变化的实现细节?若有,必须收进 private RPC 或重新设计公开接口;
- 审视设备边界:新功能在低端 ARM 设备上是否仍可用?处理逻辑是否尽量放到了客户端?
- 实验性功能走专用通道:标记为 private 或文档注明“实验性、可无通知废弃”,并写明过期日期,保持短生命周期;
- 联系维护者:通过
dart-service-protocol@google.com(或 VM 实现相关问题联系dart-vm-team@google.com)沟通。
七、延伸阅读
- runtime/vm/service/service.md:Dart VM Service Protocol 4.22 完整规范,包括全部公开 RPC、事件、类型定义与版本历史;
- runtime/vm/service/service_extension.md:通过 Dart 核心库暴露的服务协议扩展(如 dart:io 的 socket/HTTP 剖析扩展),方法名以
ext.dart.libraryName前缀调用; - runtime/vm/service/service.cc:协议实现的注册表与全部 RPC 处理函数;
- runtime/vm/service/service_event.cc:
VMFlagUpdate等事件序列化的实现; - runtime/vm/profiler.cc:
profile_period等标志定义与采样配置归一化逻辑。
- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
相关推荐
dds_service_extensions 包详解:扩展 Dart VM Service 接口,对接 DDS 协议 RPC
dds_service_extensions 包详解:扩展 Dart VM Service 接口,对接 DDS 协议 RPC dds_service_exten
编程语言编译器语言运行时标准库开发工具Dart Development Service (DDS) 协议 2.1 详解:VM Service 之上的开发服务扩展协议
Dart Development Service DDS 协议 2.1 详解:VM Service 之上的开发服务扩展协议 本篇技术指南系统讲解 Dart SD
编程语言编译器语言运行时标准库开发工具深入解析 Dart VM Service Protocol:基于 Dart SDK 源码的协议规范、RPC 实战与调试工具开发指南
深入解析 Dart VM Service Protocol:基于 Dart SDK 源码的协议规范、RPC 实战与调试工具开发指南 本文以 Dart SDK 仓
编程语言编译器语言运行时标准库开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考