跳转至

JieLabs 是如何工作的

简介

JieLabs 是陈嘉杰、高一川、刘晓义(按姓氏拼音首字母排序)于 2020 年新型冠状病毒疫情期间开发的在线数字逻辑电路实验系统,用于清华大学 2020 年春季学期数字逻辑电路实验课程。其包括前端、后端和固件三部分,分别主要由刘晓义、陈嘉杰和高一川负责开发。核心功能实现用时一周,后续界面和稳定性优化用时两周。本文会详细地介绍 JieLabs 的工作原理和一些技术细节,希望对各位同学有所帮助。

太长;不看。

采用了如下的技术方案:

前端:React 框架 + Redux 状态管理 + Monaco 编辑器 + WebAssembly 运行 Rust 代码 + WebSocket 实时通信 + SASS 样式

后端:Actix-Web 框架 + Diesel/PostgreSQL 数据库 + Redis 消息队列 + Quartus 构建 + Kubernetes 构建容器编排

固件:Xilinx FPGA 控制 + Buildroot 系统 + Linux 内核

前端

前端大部分都是刘晓义同学编写的,也是这个项目工作量最大的一部分。除了本文,还可以阅读刘晓义同学自己写的总结。主要分以下几部分来谈前端的技术实现:

第三方库

整体上采用了时下比较流行的 React 框架,配合 Redux 进行状态管理,用 React Hooks 编写组件的逻辑。为了实现 VHDL/Verilog 代码的编辑,采用了来自 VSCode 的独立编辑器空间 Monaco,并自行编写了 VHDL 和 Verilog 语言的支持,一部分在 JS 实现,另一部分在 Rust 中实现,通过 wasm-pack 打包到 JS 中执行。另外为了实现 gzip 格式的解压缩也引入了 pako 库。

在这些第三方库里,Monaco 的体积最大,后面我们针对 JS 体积做了许多优化,在下面会再提。

Rust 在前端的应用

由于开发者里刘晓义和陈嘉杰都是 Rust 语言的爱好者,考虑到目前 Rust to WASM 的技术比较成熟,WebAssembly 的可用程度也很高,我们把一些功能挪到了 Rust 中执行:

一是布线的计算。这是一个比较纯粹的算法问题,一方面对性能有一定的要求,一方面开发者比较喜欢 Rust,所以就用 Rust 实现了。这里要特别感谢刘光哲同学对布线算法的指点。在此基础上,我们用 Rust 实现了几个论文中的布线算法(Maze Router),并且通过和 JS 代码的配合得到了一个比较优秀的效果。

二是 VHDL/Verilog 的语言支持。学过编译原理的同学应该知道,如果要实现一个能够解析代码里的信号的程序,一般是不能通过正则表达式来解决的,况且我们还实现了一些错误信息的显示。VHDL 语言支持采用了已有的比较完善的库,Verilog 由于现有的库都比较庞大,不适合放于前端,于是我们编写了一个最小的 Verilog(实际上算是 SystemVerilog)的解析,仅仅足够满足我们的需求。如果同学们遇到了一些语法上功能的缺少,欢迎提出。

Canvas 的应用

连线部分因为是动态生成的,所以也是动态绘制的,Canvas 就可以派上用场了。我们也利用了 Canvas 的特性,针对每一个网络都画在一个 Canvas 上,那么在检测鼠标位置的时候,只要检查 Canvas 在鼠标所在的点上是否颜色,就知道鼠标是否在它上面了。

前端加载速度的优化

优化前前端 JS 和 WASM 总大小大约是 4MB,对于网络不好的用户来说,它的加载时间是不能容忍的。于是我们采用了以下的措施:

  1. 打开 gzip:有很显著的效果,但因为一些未知的原因,在实际部署的时候未能打开
  2. 缩小 JS 体积:通过 Webpack Analyzer 分析程序各个部分的大小,删掉了 Monaco 中一些没有用到的功能
  3. 缩小 WASM 体积:打开 LTO 和 -Os 选项
  4. 代码分割:把不同功能的代码分割开,先让一部分代码加载进来,可以绘制一个部分功能的界面,然后再继续加载剩下的组件
  5. CDN:把一部分外部的依赖放到国内,后续如果有需求的话也可以把内部的依赖也放到国内的 CDN 上

后端

后端用 Rust 语言编写,采用了目前比较成熟的 actix-web 框架,大量使用了 async/await。除此之外,用 Redis 作为消息队列,在 Docker 容器中运行 Quartus,用 Kubernetes 进行容器的动态调度。

任务调度

对于用户提交的代码和约束,后端需要进行任务的调度,生成一个新的任务,放入到 Redis 消息队列中。另一方面,Docker 中运行的 python 脚本会从 Redis 中取任务,任务完成后把结果上传并回传给后端表示确认。如果一个任务一直没有完成,后端会进行回收并重新分配一个任务到队列中。为了防止这个过程中出现重复任务的提交,为每个提交设置了一个足够长的随机 ID。Docker 容器一开始是通过 docker-compose 进行配置,后来考虑到这个场景比较适合 kubernetes,于是使用了一下,还挺好用的。一开始用的是 minikube,搭好 docker registry,然后往里面部署几个 pod 并设置 hpa,具体可以看我的另一篇博客,后来迁移到了 kubeadm 直接配置。现在迁移到了一个 k3s 搭建的 k8s 集群上。

板子通信

第二个主要功能是进行板子的分配和通信。每个用户会创建一个 WebSocket 连接到后端,每个板子也是一个 WebSocket。当一个用户分配到一个板子的时候,它可以通过后端发送命令给对应的板子,板子的回复也会原路返回,相当于一个 WebSocket Proxy。另外为了保证资源的利用率,添加了一些限制、心跳包和认证。

状态监控

为了可以直观地看到各个数据,实现了一个简单的监控接口,接入 Telegraf+InfluxDB+Grafana 的监控系统,可以实时看到各个资源的情况,如用户、板子和任务等等,也方便我们在在线用户比较少的时候进行更新。

板子

这个平台虽然是用于数字逻辑实验课程,但实际用的板子来自数字逻辑设计课程。我们把其上一个 Altera FPGA 作为实验 FPGA,在控制的 Xilinx FPGA 上运行我们的固件,负责读取和写入 GPIO、下载 bitstream 等等功能。

评论