Skip to content

Commit

Permalink
feat(status): refactor server, expose app.webui
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Mar 29, 2021
1 parent c8c7ad8 commit a8ff8a6
Show file tree
Hide file tree
Showing 14 changed files with 622 additions and 758 deletions.
2 changes: 1 addition & 1 deletion packages/plugin-eval/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class Sandbox {
codeGeneration: { strings, wasm },
})

const filename = resolve(__dirname, '../dist/internal.js')
const filename = resolve(__dirname, '../lib/internal.js')
const data = readFileSync(filename, 'utf8')
const script = new Script(data, { filename })

Expand Down
8 changes: 4 additions & 4 deletions packages/plugin-status/client/views/home/home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<k-numeric title="当前消息频率" icon="paper-plane">{{ currentRate }} / min</k-numeric>
<k-numeric title="近期消息频率" icon="history">{{ recentRate }} / d</k-numeric>
<k-numeric title="命名插件数量" icon="plug">{{ registry.pluginCount }}</k-numeric>
<k-numeric title="数据库体积" icon="database">{{ (profile.storageSize / 1048576).toFixed(1) }} MB</k-numeric>
<k-numeric title="活跃用户数量" icon="heart">{{ profile.activeUsers }}</k-numeric>
<k-numeric title="活跃群数量" icon="users">{{ profile.activeGroups }}</k-numeric>
<k-numeric title="数据库体积" icon="database">{{ (meta.storageSize / 1048576).toFixed(1) }} MB</k-numeric>
<k-numeric title="活跃用户数量" icon="heart">{{ meta.activeUsers }}</k-numeric>
<k-numeric title="活跃群数量" icon="users">{{ meta.activeGroups }}</k-numeric>
</div>
<load-chart/>
<div class="card-grid chart-grid">
Expand All @@ -18,7 +18,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { stats, profile, registry } from '~/client'
import { stats, profile, meta, registry } from '~/client'
import GroupChart from './group-chart.vue'
import HistoryChart from './history-chart.vue'
import HourChart from './hour-chart.vue'
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-status/client/views/plugins/plugin-view.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@

<script setup lang="ts">
import type { PluginData } from '~/server'
import type { Registry } from '~/server'
import { ref, computed, defineProps } from 'vue'
const show = ref(false)
const props = defineProps<{ data: PluginData }>()
const props = defineProps<{ data: Registry.PluginData }>()
const state = computed(() => {
return props.data.sideEffect ? 'side-effect' : 'normal'
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-status/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "lib/index.js",
"typings": "lib/index.d.ts",
"files": [
"dist",
"lib",
"lib"
],
"author": "Shigma <1700011071@pku.edu.cn>",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-status/server/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Adapter, App, Bot, Context, Logger, omit, pick, Random, Session, Time, User } from 'koishi-core'
import { Profile } from './profile'
import { Profile } from './data'
import WebSocket from 'ws'

const logger = new Logger('status')
Expand Down
230 changes: 230 additions & 0 deletions packages/plugin-status/server/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { Assets, Bot, Context, Platform, Plugin, Time } from 'koishi-core'
import { cpus } from 'os'
import { mem } from 'systeminformation'

export interface DataSource<T = any> {
get(forced?: boolean): Promise<T>
}

export type LoadRate = [app: number, total: number]
export type MessageRate = [send: number, receive: number]

let usage = getCpuUsage()
let appRate: number
let usedRate: number

async function memoryRate(): Promise<LoadRate> {
const { total, active } = await mem()
return [process.memoryUsage().rss / total, active / total]
}

function getCpuUsage() {
let totalIdle = 0, totalTick = 0
const cpuInfo = cpus()
const usage = process.cpuUsage().user

for (const cpu of cpuInfo) {
for (const type in cpu.times) {
totalTick += cpu.times[type]
}
totalIdle += cpu.times.idle
}

return {
// microsecond values
app: usage / 1000,
// use total value (do not know how to get the cpu on which the koishi is running)
used: (totalTick - totalIdle) / cpuInfo.length,
total: totalTick / cpuInfo.length,
}
}

function updateCpuUsage() {
const newUsage = getCpuUsage()
const totalDifference = newUsage.total - usage.total
appRate = (newUsage.app - usage.app) / totalDifference
usedRate = (newUsage.used - usage.used) / totalDifference
usage = newUsage
}

export interface BotData {
username: string
selfId: string
platform: Platform
code: Bot.Status
currentRate: MessageRate
}

function accumulate(record: number[]) {
return record.slice(1).reduce((prev, curr) => prev + curr, 0)
}

export async function BotData(bot: Bot) {
return {
platform: bot.platform,
selfId: bot.selfId,
username: bot.username,
code: await bot.getStatus(),
currentRate: [accumulate(bot.messageSent), accumulate(bot.messageReceived)],
} as BotData
}

export class Profile implements DataSource<Profile.Payload> {
cached: Profile.Payload

constructor(private ctx: Context, config: Profile.Config) {
this.apply(ctx, config)

ctx.on('status/tick', async () => {
this.ctx.app.webui.adapter?.broadcast('profile', await this.get(true))
})
}

async get(forced = false) {
if (this.cached && !forced) return this.cached
const [memory, bots] = await Promise.all([
memoryRate(),
Promise.all(this.ctx.bots.filter(bot => bot.platform !== 'web').map(BotData)),
])
const cpu: LoadRate = [appRate, usedRate]
return { bots, memory, cpu }
}

static initBot(bot: Bot) {
bot.messageSent = new Array(61).fill(0)
bot.messageReceived = new Array(61).fill(0)
}

private apply(ctx: Context, config: Profile.Config = {}) {
const { tickInterval } = config

ctx.all().before('send', (session) => {
session.bot.messageSent[0] += 1
})

ctx.all().on('message', (session) => {
session.bot.messageReceived[0] += 1
})

ctx.on('connect', async () => {
ctx.bots.forEach(Profile.initBot)

ctx.setInterval(() => {
updateCpuUsage()
ctx.bots.forEach(({ messageSent, messageReceived }) => {
messageSent.unshift(0)
messageSent.splice(-1, 1)
messageReceived.unshift(0)
messageReceived.splice(-1, 1)
})
ctx.emit('status/tick')
}, tickInterval)
})
}
}

export namespace Profile {
export interface Config {
tickInterval?: number
refreshInterval?: number
}

export interface Payload {
bots: BotData[]
memory: LoadRate
cpu: LoadRate
}
}

export class Meta implements DataSource<Meta.Payload> {
timestamp = 0
cachedMeta: Promise<Meta.Payload>
callbacks: Meta.Extension[] = []

constructor(private ctx: Context, public config: Meta.Config) {
this.extend(() => ctx.assets?.stats())
this.extend(() => ctx.database?.getStats())
}

async get(): Promise<Meta.Payload> {
const now = Date.now()
if (this.timestamp > now) return this.cachedMeta
this.timestamp = now + Time.hour
return this.cachedMeta = Promise
.all(this.callbacks.map(cb => cb()))
.then(data => Object.assign({}, ...data))
}

extend(callback: Meta.Extension) {
this.callbacks.push(callback)
}
}

export namespace Meta {
export interface Config {
}

export interface Stats {
allUsers: number
activeUsers: number
allGroups: number
activeGroups: number
storageSize: number
}

export interface Payload extends Stats, Assets.Stats {}

export type Extension = () => Promise<Partial<Payload>>
}

export class Registry implements DataSource<Registry.Payload> {
payload: Registry.Payload

constructor(private ctx: Context, public config: Registry.Config) {
ctx.on('registry', async () => {
this.ctx.app.webui.adapter?.broadcast('registry', await this.get(true))
})
}

async get(forced = false) {
if (this.payload && !forced) return this.payload
this.payload = { pluginCount: 0 } as Registry.Payload
this.payload.plugins = this.traverse(null)
return this.payload
}

* getDeps(state: Plugin.State): Generator<string> {
for (const dep of state.dependencies) {
if (dep.name) {
yield dep.name
} else {
yield* this.getDeps(dep)
}
}
}

traverse = (plugin: Plugin): Registry.PluginData[] => {
const state = this.ctx.app.registry.get(plugin)
const children = state.children.flatMap(this.traverse, 1)
const { name, sideEffect } = state
if (!name) return children
this.payload.pluginCount += 1
const dependencies = [...new Set(this.getDeps(state))]
return [{ name, sideEffect, children, dependencies }]
}
}

export namespace Registry {
export interface Config {
}

export interface PluginData extends Plugin.Meta {
children: PluginData[]
dependencies: string[]
}

export interface Payload {
plugins: PluginData[]
pluginCount: number
}
}

0 comments on commit a8ff8a6

Please sign in to comment.