性能指南

当领域每天使用人数达到一定规模时,部分脚本可能会出现性能问题,导致玩家感受到延迟、卡顿和报错。这是因为,在领域规模较小时编写的脚本不再适用于新的场景,通过学习编写性能良好、符合规范的代码,可以让领域重新恢复快速响应的状态。
本章将介绍一些具体情况,帮助大型领域优化在这些场景下出现的问题。
Realm 为领域提供高质量的、自动伸缩的计算和宽带资源。这些资源成本较高,出于公益目的提供,为了避免领域因为不规范的脚本占用这些资源,在执行动作时,Realm 会对运行时长、频率等资源进行限制。如果能够编写性能良好、符合最佳规范的代码,一般不会触及这个限制。
Realm 会尽可能的为所有领域在合理情况下提供充分的资源,但由于所有领域共享同一个服务器集群,在短时间内,如果出现突发的性能需求,有可能因为服务器伸缩不及时,造成短期延迟。如果经常遇到这种情况,可以通过反馈页面向我们联系。对于性能要求较高的用户,我们也可以提供独立部署的服务。

执行超时

当一个脚本的运行时长超过 1 秒,就会提示该问题。如果经常出现该问题时,需要检查:
  • 频繁读写不同的属性和物品、以及频繁读写数据库。这些操作单个平均用时在 1~3 毫秒左右,如果循环次数过多,重复进行这些操作,可能会达到运行时长上限。
  • 存在死循环、计算量过大的代码。Realm 的脚本性能充足,十万次空循环的执行时长在 500 毫秒左右,常规纯计算场景下,一般不会达到运行时长上限,往往是代码出现问题或设计不合理导致的。
  • 不正确的使用数据.目录。该命令的执行时长和该路径下所含有的节点总数成正比。随着数据的增多,该命令的执行时间会变得很长。具体解决方案可参考下面“数据操作优化”优化小节。
此外,Realm 服务器出现问题时,也可能导致脚本运行时长异常。如果出现这种情况,请通过反馈页面向我们联系。

超过了单个玩家的资源使用量

当单个玩家在 10 秒内,占用了 5 秒的运行时长时,便会提示该问题。由于单个脚本有 1 秒的运行时长限制,这意味着如果出现这个报错,玩家至少在 10 秒内执行了多次运行时间长的脚本。
当出现该问题时,需要检查:
  • 复杂的组件被反复刷新。可以通过关闭“点击按钮时刷新”并使用“刷新通道”局部更新组件缓解这个问题。
  • 延迟执行的动作在该时间段被密集执行。对于大规模的复杂领域,可以通过时间戳判断替代延迟执行。例如,通过记录和比较签到时间来判断是否签到,而不是延迟执行设置“已签到”属性的动作。
  • 活跃动作的脚本冗余。每个玩家每分钟都会执行一次活跃动作,不能在其中放入过于复杂的代码和重复进行判断的代码,更不能在其中反复调用延迟执行。例如,如果在活跃动作中添加“延迟到下一周执行”的动作,可能会让服务器在周一凌晨接收到数万条请求。

数据操作优化

不同操作有着不一样的性能,以下是常用命令的执行时长:
  • 变量读写和计算:非常快,几乎不消耗时长。
  • 属性读取:较快,读取后会缓存,后续读写更快。
  • 属性赋值:较快,赋值后会缓存,后续读写更快。
  • 数据.读:很快。
  • 数据.写:很快,连续写入时会更快。
  • 数据.目录:数据量少时较快,但随着该路径下的节点数量增加,会变得很慢。谨慎使用。
  • 排序器.查:很快。
  • 排序器.批量查:很快,执行时长随查询数量增加而增加。但比 数据.目录 快得多。
  • 排序器.写:很快。
其中,属性、数据库和排序器的读写时长不受领域规模影响。即使领域的玩家总数、数据库和排序器的数据规模不断增加,这些操作也会始终保持“很快”的速度。但是,即使是很快的操作,通过循环重复进行执行,也会需要很长的时间。
批量读取数据时,可以使用预加载提高性能:
在没有数据时,数据.目录执行速度很快,但随着路径下的节点数增加,执行时长会不断增长。如果在错误的场景下使用数据.目录,容易造成脚本超时。
解决数据.目录执行时间长的问题,有两种方案:
  • 使用排序器。当写入新值时,向排序器写入路径,删除该值时,从排序器中删除路径。读取数据目录时,使用批量查进行查询。(始终很快)
  • 额外在数据库中写入一个数组,代替数据.目录。当写入新值时,向这个数组添加对应路径。删除该值时,同时删除相应路径。读取数据目录时,直接读取该数组。(数组很长时,读写会慢)

调试执行时长

通过录制日志,可以查看每一条命令的执行时长。
如上图所示,每行倒数第二个值为该命令的执行时长(毫秒),最后一个值为执行到此处所消耗的时间。
经验上来看,代码的执行时长遵循二八定律,即 80% 的执行时长来源于 20% 的代码。通过这种方法,可以快速定位性能问题的来源。

编写代码时的性能意识

如果想要编写高性能代码,在编写代码时,应当具有“性能意识”:
  • 减少重复读写数据库和 API,避免反复读取不必要的数据。
  • 减少或不使用延迟执行。例如,避免重复添加大量延迟执行到第二天凌晨的动作。
  • 优化代码逻辑,减少循环、递归的次数。
  • 将相似的代码逻辑合并到一个函数中,当出现性能问题时,方便判断和修改。