介绍
本来呢最近是在学Rust,顺便看看Tauri相关的内容.然后刷评论区突然看到有人提到go生态中也有类似的框架—Wails,所以下午花了点时间来动手玩一下.
首先看一下最终的运行效果,前端样式懒得调整所以界面很丑只是实现一下功能
开始
这次的目标就是做一个功能类似于nvidia-smi
的桌面工具,另外整合一下cpu的使用情况.说明一下所利用到的相关库,关于CPU资源利用gopsutil来得到,GPU相关信息则是使用go-nvml(目前仅仅支持linux,原本想利用wails跨平台编译一个windows exe结果不支持就此作罢);前端界面除了wails本身的vue模板,可视化利用vue3-apexcharts.
大致思路:
- 实现后端逻辑,得到相关数据
- 实现前端界面
- 前后端交互实现动态数据更新
1 后端逻辑
根据相关库的介绍,我们可以简单实现相应数据的获取,这里拿GPU中计算利用率为例
首先我们需要一个全局的nvml device,由于主机只有一张显卡所以没有考虑利用count循环而是直接指定index(0)作为device.
ret = nvml.Init()
if ret != nvml.SUCCESS {
log.Fatalf("init failed %v", nvml.ErrorString(ret))
}
device, ret = nvml.DeviceGetHandleByIndex(0)
这里没有defer nvml.shutdown()
因为是一直更新,所以当函数退出但是goroutine依旧在运行,后期就无法得到device出错.
再来考虑数据方面,因为我想观察usage的变化,因此得设计一个类数组容器来记录每次得到的值,同时为了保证不会无限增长所以得设置最大长度.一开始考虑用数组或者切片,但是这无可避免会导致后期持续更新数据的时候每次都需要copy,无端消耗性能与内存空间.所以想到用循环队列,这样只需要更新队首与队尾,而且占用内存并不会产生增长.
type CircularQueue struct {
slice []uint32
front int
size int
count int
}
func newCircularQueue(maxSize int) *CircularQueue {
return &CircularQueue{
slice: make([]uint32, maxSize),
front: 0,
size: maxSize,
count: 0,
}
}
func (cq *CircularQueue) enqueue(element uint32) {
if cq.count == cq.size {
cq.front = (cq.front + 1) % cq.size
cq.count--
}
rear := (cq.front + cq.count) % cq.size
cq.slice[rear] = element
cq.count++
}
func (cq *CircularQueue) getSlice() []uint32 {
if cq.count == 0 {
return nil
}
slice := make([]uint32, cq.count)
for i := 0; i < cq.count; i++ {
slice[i] = cq.slice[(cq.front+i)%cq.size]
}
return slice
}
通过循环队列,我们每次请求元素并将元素入队,最终返回只需要利用getSlice()
方法返回队列中的数据即可.具体得到GPU Usage数据的代码如下
const MAXSIZE = 10
var GpuUsagecq = newCircularQueue(MAXSIZE)
func (a *App) GetGpuUsage() []uint32 {
rwmutex.RLock()
utilization, ret := device.GetUtilizationRates()
rwmutex.RUnlock()
if ret != nvml.SUCCESS {
log.Fatalf("Unable to get utilization of device at index %d: %v", 0, nvml.ErrorString(ret))
}
GpuUsagecq.enqueue(utilization.Gpu)
return GpuUsagecq.getSlice()
}
这里为了保证并发安全还是用了一下读锁,不过感觉读操作的话用不用应该问题不大.这里使用device.XXX()
就可以得到GPU当前的各种信息,具体可以去看api文档.按照相似的逻辑,就可以得到我们所需要的所有信息数据.
2 前端界面
这里我们使用vue3-apexcharts,不过由于我的前端技术很菜,对于vue3也只是大致了解过,因此这部分我也不太好详细简介,更多是对着官网中的demo修改.在frontend/src/components
下创建一个组件Monitor,然后写一下template
<template>
<div>
<div class="chart-row">
<apexchart
v-for="(chartOptions, index) in areaChartOptions"
:key="index"
type="area"
:options="chartOptions"
:series="areaChartSeries[getChartId(index)]"
class="chart-column"
/>
</div>
<div class="chart-row">
<apexchart type="donut" :options="DonutOptions" :series="donutSeries" />
<apexchart type="radialBar" :options="radialOptions" :series="radialSeries" />
</div>
</div>
</template>
在下面data()中设置好options和series初始值,运行wails dev
就能看到一个静态的页面.
3 前后端交互
这部分我感觉wails文档中写的并不好,没有任何很详细的例子指出运行时的前后端数据如何交互.参考官网介绍中唯一可以参考的Events事件配合如何工作,自己慢慢体会写出来.
首先来看后端部分,第一部分中我们实现了所有数据的获取,这里我们只需要让数据获取持续运行(刷新间隔自定义)并且将数据传给前端.
go func() {
for {
runtime.EventsEmit(a.ctx, "GetCpuUsage", a.GetCpuUsage())
runtime.EventsEmit(a.ctx, "GetGpuMem", a.GetGpuMem())
runtime.EventsEmit(a.ctx, "GetGpuUsage", a.GetGpuUsage())
time.Sleep(100 * time.Millisecond)
}
}()
go func() {
for {
runtime.EventsEmit(a.ctx, "GetFans", a.GetFans())
runtime.EventsEmit(a.ctx, "GetTemperature", a.GetTemperature())
time.Sleep(3 * time.Second)
}
}()
我希望关于usage以及memory的信息刷新更及时,而温度和风扇转速这些貌似不太重要的信息可以慢一点.然后通过EventsEmit
将数据与事件传递给前端,而前端设置一下EventsOn
事件监听,实现数据接收并完成前端界面数据更新.
EventsOn("GetGpuUsage",GetGpuUsage=>{
if(GetGpuUsage){
this.areaChartSeries['area-chart-1'][0].data=GetGpuUsage
}
})
这样就最终实现了我们一开始的界面内容.
最后
这一次尝试算是Tauri之前的一次小玩具,花了两个个小时从0学习wails以及一些前端库,最终也算拼凑出了最初设计的功能.wails最终打包出来的可执行文件大小仅仅只有2.8M,比起一些Electron打包出来的工具来说小了不止一点.
另外我们可以在main.go
中添加webview使用硬件加速,这样就可以让GPU usage不再一直是0%
Linux: &linux.Options{
WebviewGpuPolicy: linux.WebviewGpuPolicyAlways,
},
最后放一张与watch -n0.1 nvidia-smi
对比的图片作为结束